一. 聚合框架 聚合框架是MongoDB的高級查詢語言,它允許我們通過轉換和合併多個文檔中的數據來生成新的單個文檔中不存在的信息。 聚合管道操作主要包含下麵幾個部分: $lookup 在本篇幅中,我們聚焦$lookup的使用。 二. $lookup的功能及語法 1. 主要功能 是將每個輸入待處理的文 ...
一. 聚合框架
聚合框架是MongoDB的高級查詢語言,它允許我們通過轉換和合併多個文檔中的數據來生成新的單個文檔中不存在的信息。
聚合管道操作主要包含下麵幾個部分:
命令 | 功能描述 |
$project | 指定輸出文檔里的欄位. |
$match | 選擇要處理的文檔,與fine()類似。 |
$limit | 限制傳遞給下一步的文檔數量。 |
$skip | 跳過一定數量的文檔。 |
$unwind | 擴展數組,為每個數組入口生成一個輸出文檔。 |
$group | 根據key來分組文檔。 |
$sort | 排序文檔。 |
$geoNear | 選擇某個地理位置附近的的文檔。 |
$out | 把管道的結果寫入某個集合。 |
$redact | 控制特定數據的訪問。 |
$lookup |
多表關聯(3.2版本新增) |
在本篇幅中,我們聚焦$lookup的使用。
二. $lookup的功能及語法
1. 主要功能 是將每個輸入待處理的文檔,經過$lookup 階段的處理,輸出的新文檔中會包含一個新生成的數組列(戶名可根據需要命名新key的名字 )。數組列存放的數據 是 來自 被Join 集合的適配文檔,如果沒有,集合為空(即 為[ ])
2. 基本語法
{ $lookup: { from: <collection to join>, localField: <field from the input documents>, foreignField: <field from the documents of the "from" collection>, as: <output array field> } }
3. 語法的解釋說明
語法值 | 解釋說明 |
from
|
同一個資料庫下等待被Join的集合。 |
localField
|
源集合中的match值,如果輸入的集合中,某文檔沒有 localField 這個Key(Field),在處理的過程中,會預設為此文檔含 有 localField:null的鍵值對。 |
foreignField
|
待Join的集合的match值,如果待Join的集合中,文檔沒有foreignField 值,在處理的過程中,會預設為此文檔含有 foreignField:null的鍵值對。 |
as
|
為輸出文檔的新增值命名。如果輸入的集合中已存在該值,則會覆蓋掉, |
(註:null = null 此為真)
其語法功能類似於下麵的偽SQL語句:
SELECT *, <output array field> FROM collection WHERE <output array field> IN (SELECT * FROM <collection to join> WHERE <foreignField>= <collection.localField>);
三. 案例
以上的語法介紹有些枯燥,不易理解,我們直接分析品味案例好了。
假設 有 訂單集合, 存儲的測試數據 如下:
db.orders.insert([ { "_id" : 1, "item" : "almonds", "price" : 12, "quantity" : 2 }, { "_id" : 2, "item" : "pecans", "price" : 20, "quantity" : 1 }, { "_id" : 3 } ])
其中 item 對應 數據為 商品名稱。
另外 一個 就是就是 商品庫存集合 ,存儲的測試數據 如下:
db.inventory.insert([ { "_id" : 1, "sku" : "almonds", description: "product 1", "instock" : 120 }, { "_id" : 2, "sku" : "bread", description: "product 2", "instock" : 80 }, { "_id" : 3, "sku" : "cashews", description: "product 3", "instock" : 60 }, { "_id" : 4, "sku" : "pecans", description: "product 4", "instock" : 70 }, { "_id" : 5, "sku": null, description: "Incomplete" }, { "_id" : 6 } ])
此集合中的 sku 數據等同於 訂單 集合中的 商品名稱。
在這種模式設計下,如果要查詢訂單表對應商品的庫存情況,應如何寫代碼呢?
很明顯這需要兩個集合Join。
場景簡單,不做贅述,直送答案 。其語句 如下:
db.orders.aggregate([ { $lookup: { from: "inventory", localField: "item", foreignField: "sku", as: "inventory_docs" } } ])
返回的執行結果如下:
{ "_id" : NumberInt("1"), "item" : "almonds", "price" : NumberInt("12"), "quantity" : NumberInt("2"), "inventory_docs" : [ { "_id" : NumberInt("1"), "sku" : "almonds", "description" : "product 1", "instock" : NumberInt("120") } ] } { "_id" : NumberInt("2"), "item" : "pecans", "price" : NumberInt("20"), "quantity" : NumberInt("1"), "inventory_docs" : [ { "_id" : NumberInt("4"), "sku" : "pecans", "description" : "product 4", "instock" : NumberInt("70") } ] } { "_id" : NumberInt("3"), "inventory_docs" : [ { "_id" : NumberInt("5"), "sku" : null, "description" : "Incomplete" }, { "_id" : NumberInt("6") } ] }
分析查詢語句和結果,回扣$lookup定義,可以將上面的處理過程,描述如下:
從集合order中逐個獲取文檔處理,拿到一個文檔後,會根據localField 值 遍歷 被 Join的 inventory集合(from: "inventory"),看inventory集合文檔中 foreignField值是否與之相等。如果相等,就把符合條件的inventory文檔 整體 內嵌到聚合框架新生成的文檔中,並且新key 統一命名為 inventory_docs。考慮到符合條件的文檔不唯一,這個Key對應的Value是個數組形式。原集合中Key對應的值為Null值或不存在時,需特別小心。
四. 說明
在以下的說明中,為描述方便,將 from對應的集合定義為 被join集合;待聚合的表成為源表; 將 localField 和 foreignField 對應的Key 定義 比較列。
1. 因客戶端工具顯示的問題,上面示例中查詢結果重Int 類型值都自動顯示為了 NumberInt("")。這個NumberInt標註,請忽略,不影響我們的功能測試。
2. 這個示例中,一共輸出了三個文檔,在沒有再次聚合($match)的條件下,這個輸出文檔數量是以輸入文檔的數量來決定的(由order來決定),而不是以被Join的集合(inventory)文檔數量決定。
3. 在此需要特別強調的是輸出的第三個文檔。在源庫中原文檔沒有要比較的列(即item值不存在,既不是Null值,也不是值為空),此時 和 被Join 集合比較,如果 被Join集合中 比較列 也恰好 為NUll 或 不存在的值,此時,判斷相等 ,即會把 被Join集合中 比較列 為NUll 或 值不存在 文檔 吸收進來。
4. 假設 源表(order) 中比較列 為某一個值,而此值在待比較表(inventory)的所有文檔中都不存在,那麼查詢結果會是什麼樣子呢?
order 集合在現有數據的基礎上,再被insert 進一筆測試數據,這個訂單的商品為 Start,在庫存商品中根本沒有此數據。
db.orders.insert({"_id" : 4, "item" : "Start", "price" : 2000, "quantity" : 1 })
order集合的文檔數量由之前的3個增長為4個。
再次執行查詢
db.orders.aggregate([ { $lookup: { from: "inventory", localField: "item", foreignField: "sku", as: "inventory_docs" } } ])
此時查看結果
{ "_id" : NumberInt("1"), "item" : "almonds", "price" : NumberInt("12"), "quantity" : NumberInt("2"), "inventory_docs" : [ { "_id" : NumberInt("1"), "sku" : "almonds", "description" : "product 1", "instock" : NumberInt("120") } ] } { "_id" : NumberInt("2"), "item" : "pecans", "price" : NumberInt("20"), "quantity" : NumberInt("1"), "inventory_docs" : [ { "_id" : NumberInt("4"), "sku" : "pecans", "description" : "product 4", "instock" : NumberInt("70") } ] } { "_id" : NumberInt("3"), "inventory_docs" : [ { "_id" : NumberInt("5"), "sku" : null, "description" : "Incomplete" }, { "_id" : NumberInt("6") } ] } { "_id" : NumberInt("4"), "item" : "Start", "price" : NumberInt("2000"), "quantity" : NumberInt("1"), "inventory_docs" : [ ] }
查詢出的結果也由之前的3個變成了4個。比較特別的是第四個文檔 ,其新增列 為 "inventory_docs" : [ ] ,即值為空 。所以,此時,實現的功能非常像關係型資料庫的 left join。
那麼,可不可以只篩選出新增列為空的文檔呢?
即 我們查詢出 ,比較列的條件下,刷選出只在A集合中,而不在集合B的文檔呢? 就像關係資料庫中量表Join的 left join on a.key =b.key where b.key is null .
答案是可以的。
其實回到聚合框架上來,再次聚合一下就可以了,來一次$match就可以了。
執行的語句調整一下就OK了。
db.orders.aggregate([ { $lookup: { from: "inventory", localField: "item", foreignField: "sku", as: "inventory_docs" } }, { $match : {"inventory_docs" : [ ]} } ])
執行結果 為
{ "_id" : NumberInt("4"), "item" : "Start", "price" : NumberInt("2000"), "quantity" : NumberInt("1"), "inventory_docs" : [ ] }
可以看出執行結果只有一個文檔。這個文檔表明的含義是:訂單中有這個商品,但是庫存中沒有這個商品。
($look只是聚合框架的一個stage,在其前前後後,都可以嵌入到其他的聚合管道的命令,例如$match.$group等。下麵的說明5,也可以說明一二)
5. 以上的比較列都是單一的Key/Value,如果複雜一點,如果比較的列是數組,我們又該如何關聯呢?
我們接下來再來測試一把。將之前 集合order 、inventory 插入的數據清空。
插入此場景下的新數據,向order中插入的數據,如下:
db.orders.insert({ "_id" : 1, "item" : "MON1003", "price" : 350, "quantity" : 2, "specs" :[ "27 inch", "Retina display", "1920x1080" ], "type" : "Monitor" })
specs 對應的value是數組格式。
向集合inventory 新插入的數據 如下:
db.inventory.insert({ "_id" : 1, "sku" : "MON1003", "type" : "Monitor", "instock" : 120,"size" : "27 inch", "resolution" : "1920x1080" }) db.inventory.insert({ "_id" : 2, "sku" : "MON1012", "type" : "Monitor", "instock" : 85,"size" : "23 inch", "resolution" : "1280x800" }) db.inventory.insert({ "_id" : 3, "sku" : "MON1031", "type" : "Monitor", "instock" : 60,"size" : "23 inch", "display_type" : "LED" })
查詢的語句如下:
db.orders.aggregate([ { $unwind: "$specs" }, { $lookup: { from: "inventory", localField: "specs", foreignField: "size", as: "inventory_docs" } }, { $match: { "inventory_docs": { $ne: [] } } } ])
查詢顯示結果如下:
{ "_id" : NumberInt("1"), "item" : "MON1003", "price" : NumberInt("350"), "quantity" : NumberInt("2"), "specs" : "27 inch", "type" : "Monitor", "inventory_docs" : [ { "_id" : NumberInt("1"), "sku" : "MON1003", "type" : "Monitor", "instock" : NumberInt("120"), "size" : "27 inch", "resolution" : "1920x1080" } ] }
仔細看啊,輸出文檔中的 specs 對應的數據變成了字元串類型(原集合為數組)。是什麼發揮瞭如此神奇功效???,請看黑板,請將目光集中在
{ $unwind: "$specs" }
還有個小問題,大家猜一下,如果查詢語句中沒有
{ $match: { "inventory_docs": { $ne: [] } } }
結果會是什麼樣呢?即查看語句修改為:
db.orders.aggregate([ { $unwind: "$specs" }, { $lookup: { from: "inventory", localField: "specs", foreignField: "size", as: "inventory_docs" } } ])
大家猜猜看!
大家猜猜看!
大家猜猜看!
呵呵...此時的結果是:
文檔1 { "_id" : NumberInt("1"), "item" : "MON1003", "price" : NumberInt("350"), "quantity" : NumberInt("2"), "specs" : "27 inch", "type" : "Monitor", "inventory_docs" : [ { "_id" : NumberInt("1"), "sku" : "MON1003", "type" : "Monitor", "instock" : NumberInt("120"), "size" : "27 inch", "resolution" : "1920x1080" } ] } 文檔2 { "_id" : NumberInt("1"), "item" : "MON1003", "price" : NumberInt("350"), "quantity" : NumberInt("2"), "specs" : "Retina display", "type" : "Monitor", "inventory_docs" : [ ] } 文檔3 { "_id" : NumberInt("1"), "item" : "MON1003", "price" : NumberInt("350"), "quantity" : NumberInt("2"), "specs" : "1920x1080", "type" : "Monitor", "inventory_docs" : [ ] }
你推算出正確結果了嗎?
謝謝!!!
希望以上的講解和演示能對大家學習$lookup有所幫助。
註:以上案例數據參考MongoDB官方網站,大家也可訪問官網獲取更多、更全的相關知識。
本文版權歸作者所有,未經作者同意不得轉載,謝謝配合!!!