聚合实战:多阶段组合
本教程共 50 篇 · 第 35 篇 · 更新于 2026-07-30 · 约 9 分钟阅读
35. 聚合实战:多阶段组合
本节目标:把前面学的阶段串成完整报表——算各用户的订单总额(带用户名)、算各类商品的销量,以及看订单的月度分布,体会一个真实聚合管道怎么长出来。
前面每章都只讲一个阶段。真实报表往往是好几个阶段接力完成的。这章用三张表做一个电商统计后台,把 $match、$lookup、$unwind、$group、$sort、$project、$count 串起来。
先把数据铺好,后面所有示例都基于它:
db.users.insertMany([
{ _id: 1, name: "张三", city: "北京" },
{ _id: 2, name: "李四", city: "上海" },
{ _id: 3, name: "王五", city: "北京" }
])
db.products.insertMany([
{ _id: 101, name: "机械键盘", category: "外设", price: 399 },
{ _id: 102, name: "无线鼠标", category: "外设", price: 129 },
{ _id: 103, name: "27寸显示器", category: "显示器", price: 1099 }
])
db.orders.insertMany([
{ _id: 1001, user_id: 1, status: "completed", total: 657,
created_at: ISODate("2024-06-01"),
items: [ { product: "机械键盘", qty: 1 }, { product: "无线鼠标", qty: 2 } ] },
{ _id: 1002, user_id: 2, status: "completed", total: 1099,
created_at: ISODate("2024-06-05"),
items: [ { product: "27寸显示器", qty: 1 } ] },
{ _id: 1003, user_id: 1, status: "shipped", total: 597,
created_at: ISODate("2024-06-10"),
items: [ { product: "机械键盘", qty: 1 } ] },
{ _id: 1004, user_id: 3, status: "pending", total: 528,
created_at: ISODate("2024-07-12"),
items: [ { product: "无线鼠标", qty: 1 }, { product: "机械键盘", qty: 1 } ] }
])
报表一:各用户的订单总额(带用户名)
思路:先按用户汇总金额,再 $lookup 关联用户名,最后整形。
db.orders.aggregate([
// 1. 只统计已完成的订单
{ $match: { status: "completed" } },
// 2. 按用户分组,算每人的总额和订单数
{
$group: {
_id: "$user_id",
totalAmount: { $sum: "$total" },
orderCount: { $sum: 1 }
}
},
// 3. 关联 users 表取名字
{
$lookup: {
from: "users",
localField: "_id",
foreignField: "_id",
as: "user"
}
},
// 4. 把名字从数组里取出来,并重命名字段
{
$project: {
_id: 0,
userName: { $arrayElemAt: [ "$user.name", 0 ] },
totalAmount: 1,
orderCount: 1
}
},
// 5. 按总额降序
{ $sort: { totalAmount: -1 } }
])
结果是 [ { userName: "张三", totalAmount: 1254, orderCount: 2 }, { userName: "李四", totalAmount: 1099, orderCount: 1 } ]。
Note
$lookup的as: "user"是数组,用$arrayElemAt: [..., 0]取第一条。这是关联后取单值的常用套路。
报表二:各类商品的销量
思路:先 $unwind 拆商品行,再关联商品表拿分类,最后按分类汇总数量。
db.orders.aggregate([
// 1. 拆开订单里的每个商品行
{ $unwind: "$items" },
// 2. 按商品名关联 products,取分类
{
$lookup: {
from: "products",
localField: "items.product",
foreignField: "name",
as: "p"
}
},
// 3. 把分类从数组里取出来,方便后续分组
{
$project: {
category: { $arrayElemAt: [ "$p.category", 0 ] },
qty: "$items.qty"
}
},
// 4. 按分类汇总销量
{
$group: {
_id: "$category",
totalQty: { $sum: "$qty" }
}
},
// 5. 按销量降序
{ $sort: { totalQty: -1 } }
])
结果形如 { _id: "外设", totalQty: 6 }、{ _id: "显示器", totalQty: 1 }。
Tip拆数组(
$unwind)→ 关联($lookup)→ 取字段($project)→ 分组($group),这是处理「数组元素 + 另一张表」的固定四步曲。
报表三:订单的月度分布
思路:用日期表达式取出月份,再按月份分组计数。
db.orders.aggregate([
// 1. 从 created_at 取年月作为分组键
{
$group: {
_id: {
year: { $year: "$created_at" },
month: { $month: "$created_at" }
},
orderCount: { $sum: 1 }
}
},
// 2. 按时间升序
{ $sort: { "_id.year": 1, "_id.month": 1 } }
])
结果形如 { _id: { year: 2024, month: 6 }, orderCount: 3 }、{ _id: { year: 2024, month: 7 }, orderCount: 1 }。
报表四:总订单数(用 $count)
db.orders.aggregate([
{ $match: { status: { $ne: "cancelled" } } },
{ $count: "validOrders" }
])
这里用 $count 一步得到「未取消订单」的总数。
一条流水线的思考顺序
写复杂聚合时,我习惯按这个顺序搭:
$match先缩数据(能用索引,最快)。$unwind/$lookup展开或关联。$group做汇总。$sort排序。$skip+$limit分页。$project/$count最后整形输出。
Warning阶段越靠前,处理的文档越少、越省资源。把
$match、$project(减字段)尽量往前放,是聚合性能的基本功。
到这里,MongoDB 的查询、更新、删除与聚合框架的常用阶段我们都过完了。把这些组合好,绝大多数数据统计需求都能在数据库里一站搞定。