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。