HOWTO · MongoDB

使用 MongoDB 按多個欄位對值進行分組

在本文中,我們將檢視與聚合一起使用的運算子列表,以及如何藉助特定示例來實現它們。

本頁內容

若要依多個欄位將 MongoDB 文件分組,請將 $group._id 設為包含各欄位運算式的文件,例如 {item: "$item", inv_date: "$inv_date"}。MongoDB 會針對每一種不同的欄位值組合建立一份結果文件。$sum、$avg 和 $push 等累加器可在各群組內計算或收集值。

MongoDB 的 $group 如何依鍵建立結果

聚合管道會依序將文件傳遞給各個階段。$group 會依據必要的 _id 運算式,合併它收到的文件,並為每個不同的鍵輸出一份結果文件。欄位運算式會依單一值分組;包含多個運算式的文件則會建立複合鍵。輸出文件的 _id 會包含該鍵。

累加器會針對每個群組計算值。$sum: 1 會計算文件數;$sum 搭配數值欄位或運算式會加總數值;$avg 則會計算數值的平均值。$multiply 可先計算每份文件的金額,再由 $sum 加總。$push 會將每份輸入文件中的值加入陣列;$addToSet 則會收集不重複的值。$min、$max、$first 和 $last 會依各自的累加器規則選取值。如果選取結果或陣列順序必須明確,請先排序輸入文件。

以下 MongoDB 範例使用三份發票文件:兩份蘋果發票的日期為 2020-02-02,數量分別是 20 和 5,單價皆為 2;另一份蘋果發票的日期為 2020-04-13,數量為 30,單價為 3。假設 inv_date 一律以字串儲存,而 qty 和 rate 都是數值。以下聚合片段僅供說明,並未在 MongoDB 伺服器上執行。請將示範的階段或管道傳入 db.invoice.aggregate([...])。

獨立的 Python 範例使用相同的三筆發票資料示範複合分組。它說明複合鍵如何將兩個日期分開;此範例不會連線至 MongoDB。程式碼已執行,並產生下方所示的輸出。

執行此範例:

本節中的程式碼以及顯示的標籤或輸出仍為英文。

invoices = [
    {"item": "apple", "inv_date": "2020-02-02", "qty": 20},
    {"item": "apple", "inv_date": "2020-02-02", "qty": 5},
    {"item": "apple", "inv_date": "2020-04-13", "qty": 30},
]
groups = {}
for invoice in invoices:
    key = (invoice["inv_date"], invoice["item"])
    groups.setdefault(key, {"count": 0, "totalQty": 0})
    groups[key]["count"] += 1
    groups[key]["totalQty"] += invoice["qty"]
for key, totals in groups.items():
    print({"_id": {"inv_date": key[0], "item": key[1]}, **totals})
{'_id': {'inv_date': '2020-02-02', 'item': 'apple'}, 'count': 2, 'totalQty': 25}
{'_id': {'inv_date': '2020-04-13', 'item': 'apple'}, 'count': 1, 'totalQty': 30}

此 Python 範例已執行,並產生上述兩筆分組結果。本文中的 MongoDB 聚合片段並未在 MongoDB 伺服器上執行。

若要依日期進行單一欄位摘要,請使用 { $group: { _id: "$inv_date", totalCost: { $sum: { $multiply: ["$rate", "$qty"] } }, avgQty: { $avg: "$qty" }, count: { $sum: 1 } } }。示意結果中,2020-02-02 的 totalCost: 50、avgQty: 12.5、count: 2;2020-04-13 的 totalCost: 90、avgQty: 30、count: 1。$multiply 會先計算每張發票的單價乘以數量,再由 $sum 加總這些金額。

依多個欄位將文件分組

若要依發票日期和品項分組,請使用 { $group: { _id: { inv_date: "$inv_date", item: "$item" }, totalCost: { $sum: { $multiply: ["$rate", "$qty"] } }, avgQty: { $avg: "$qty" }, count: { $sum: 1 } } }。這兩個值會共同組成鍵,因此只有兩者都相同的發票才會歸入同一筆結果。以範例資料來說,{inv_date: "2020-02-02", item: "apple"} 的示意結果為 {totalCost: 50, avgQty: 12.5, count: 2};2020-04-13 的蘋果群組則為 {totalCost: 90, avgQty: 30, count: 1}。日期和品項會一起出現在 _id 中,每個群組的累加器則各自計算。

_id 中的欄位名稱會標示輸出鍵的欄位;例如 "$item" 這類運算式會讀取輸入文件中的值。請依照你的資料結構修改這些運算式。依多個值分組時,請將鍵保留為文件。

在分組前使用 $match 篩選

若只想讓部分發票參與計算,請將 $match 放在 $group 前面:[{ $match: { inv_date: "2020-02-02" } }, { $group: { _id: { inv_date: "$inv_date", item: "$item" }, totalCost: { $sum: { $multiply: ["$rate", "$qty"] } }, avgQty: { $avg: "$qty" }, count: { $sum: 1 } } }]。以範例資料來說,示意結果是 2020-02-02 的蘋果群組,包含 totalCost: 50、avgQty: 12.5 和 count: 2;四月的發票會在累加器執行前排除。

階段順序會改變計算結果,因為被排除的文件不會影響總額、平均值或筆數。比對值的型別和表示方式必須與儲存欄位相同。如果 inv_date 是 BSON Date,請使用日期值比對,而不是格式化後的字串。放在分組後的 $match 只能篩選群組結果,無法改變哪些來源文件已參與累加器計算。

使用 $group 回傳不重複值

若要取得不重複的品項值,請使用 [{ $group: { _id: "$item" } }]。在範例發票中,三筆資料的品項都相同,因此示意結果只有一份群組文件 {_id: "apple"}。$group 回傳的是文件,而不是單純的字串陣列。加入 $project 可重新命名欄位,加入 $sort 則可排序結果。

使用 $push 收集各群組中的值

若要收集每個品項的發票日期,請使用 [{ $group: { _id: "$item", invoiceDates: { $push: "$inv_date" } } }]。以範例資料來說,示意結果為 {_id: "apple", invoiceDates: ["2020-02-02", "2020-02-02", "2020-04-13"]}。此例以 item 為分組鍵,並由 $push 保留每個日期,包括重複日期。陣列順序會依照文件抵達 $group 的順序;若需要明確的日期順序,請在分組前加入 $sort。若只需要不重複的日期,請改用 $addToSet。