MongoDB 基础与聚合:文档模型与管道操作
MongoDB 基础与聚合:文档模型与管道操作
认识 MongoDB 与文档模型
MongoDB 是一个开源的、面向文档的 NoSQL 数据库。与传统的关系型数据库不同,它使用类似 JSON 的 BSON 格式存储数据,每一行记录被称为一个文档,同一类文档的集合叫作集合。这种模型让数据结构更灵活,特别适合快速开发、海量数据存储和实时分析场景。
为什么选择文档模型?
- 灵活的模式:同一个集合里的文档可以有不同的字段,无需提前定义表结构。
- 嵌套子文档与数组:可以直接将关联数据内嵌到一个文档中,避免复杂的多表 JOIN。
- 与编程语言自然映射:文档结构与 JavaScript 对象、Python 字典等形式一致,开发效率更高。
一个典型 MongoDB 文档长这样:
{
"_id": "5f8d0d55b54764421b7156c2",
"name": "张三",
"age": 28,
"email": "zhangsan@example.com",
"address": {
"city": "上海",
"street": "南京路 100 号"
},
"hobbies": ["阅读", "骑行", "编程"]
}
环境准备
教程推荐使用 MongoDB Atlas 免费云实例,或者本地安装 MongoDB Community Server。连接后可以使用 MongoDB Shell(mongosh)或官方图形化工具 Compass 来执行后续所有命令。
CRUD 基础速览
在深入聚合之前,先快速回顾最基本的增删改查操作。
插入文档
// 插入单条
db.users.insertOne({ name: "李四", age: 22 });
// 插入多条
db.users.insertMany([
{ name: "王五", age: 30, hobbies: ["跑步"] },
{ name: "赵六", age: 25, address: { city: "北京" } }
]);
查询文档
// 查询所有
db.users.find();
// 条件查询
db.users.find({ age: { $gte: 25 } });
// 只返回部分字段
db.users.find({}, { name: 1, age: 1, _id: 0 });
更新文档
// 修改单条
db.users.updateOne(
{ name: "李四" },
{ $set: { age: 23, email: "lisi@example.com" } }
);
// 替换整条文档
db.users.replaceOne(
{ name: "王五" },
{ name: "王五", age: 31, city: "上海" }
);
删除文档
db.users.deleteOne({ name: "赵六" });
db.users.deleteMany({ age: { $lt: 18 } });
这些基本操作足以支撑大多数简单应用,但真正释放 MongoDB 威力的是它的聚合框架。
聚合管道:像流水线一样处理数据
聚合操作是指对集合中的数据进行分组、过滤、变换和计算,最终得到有意义的结果。MongoDB 的聚合框架基于管道概念:数据会顺序经过多个阶段,每个阶段对文档进行某种转换,并将处理后结果传递给下一个阶段。
最常用的聚合命令是 aggregate(),其基本结构如下:
db.collection.aggregate([
{ $阶段名: { /* 阶段表达式 */ } },
{ $阶段名: { /* 阶段表达式 */ } },
...
])
每个阶段都以 $ 开头,常用的包括 $match、$project、$group、$sort、$limit 等。下面结合实例逐一讲解。
核心管道阶段详解
我们以一个电商订单集合为例,文档如下:
{ "_id": 1, "customer": "张三", "items": ["鼠标", "键盘"], "amount": 250, "status": "completed", "date": "2024-01-15" }
{ "_id": 2, "customer": "李四", "items": ["显示器"], "amount": 1200, "status": "completed", "date": "2024-01-16" }
{ "_id": 3, "customer": "张三", "items": ["U盘", "网线"], "amount": 180, "status": "pending", "date": "2024-01-17" }
{ "_id": 4, "customer": "王五", "items": ["键盘"], "amount": 300, "status": "completed", "date": "2024-01-18" }
$match – 过滤数据
$match 相当于普通查询中的条件过滤,用于筛选出符合条件的文档。它通常放在管道开头以减少后续处理的数据量。
db.orders.aggregate([
{ $match: { status: "completed" } }
])
输出仅包含 _id 为 1、2、4 的三条订单。请使用与 find() 相同的查询操作符,如 $gt、$in、$and 等。
$project – 重塑文档
$project 可以对字段进行选择、重命名、新增计算字段,还可以彻底改变文档结构。
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $project: {
客户: "$customer",
金额: "$amount",
含税金额: { $multiply: ["$amount", 1.13] },
物品数量: { $size: "$items" },
_id: 0
}}
])
结果:
{ "客户": "张三", "金额": 250, "含税金额": 282.5, "物品数量": 2 }
{ "客户": "李四", "金额": 1200, "含税金额": 1356, "物品数量": 1 }
{ "客户": "王五", "金额": 300, "含税金额": 339, "物品数量": 1 }
$multiply 是聚合表达式,后面章节会详细介绍。_id: 0 表示移除默认的 _id 字段。
$group – 分组聚合
$group 类似 SQL 的 GROUP BY,根据指定字段对文档分组,并计算聚合值(如总和、平均值、计数等)。_id 字段定义了分组依据,可以是某个字段值,也可以是表达式。
db.orders.aggregate([
{ $group: {
_id: "$customer",
订单数量: { $sum: 1 },
总金额: { $sum: "$amount" },
平均金额: { $avg: "$amount" }
}}
])
输出:
{ "_id": "张三", "订单数量": 2, "总金额": 430, "平均金额": 215 }
{ "_id": "李四", "订单数量": 1, "总金额": 1200, "平均金额": 1200 }
{ "_id": "王五", "订单数量": 1, "总金额": 300, "平均金额": 300 }
常见的聚合累加器包括:
$sum:求和$avg:求平均$min/$max:最小/最大值$first/$last:组内的第一个/最后一个文档$push:将组内某个字段的值收集为数组$addToSet:类似$push但自动去重
$sort – 排序
$sort 对文档进行排序,1 表示升序,-1 表示降序。
db.orders.aggregate([
{ $sort: { amount: -1 } }
])
// 按金额从大到小排列
$limit 与 $skip – 限制与偏移
// 取前两条
db.orders.aggregate([
{ $sort: { amount: -1 } },
{ $limit: 2 }
])
// 跳过第一条,从第二条开始取
db.orders.aggregate([
{ $sort: { date: 1 } },
{ $skip: 1 },
{ $limit: 2 }
])
$unwind – 展开数组
$unwind 将数组字段拆分成多条文档,每条文档包含数组中的一个元素。这在需要基于数组内元素做进一步分组或筛选时非常有用。
假设我们想统计每种商品被订购的总次数:
db.orders.aggregate([
{ $unwind: "$items" },
{ $group: {
_id: "$items",
订购次数: { $sum: 1 }
}},
{ $sort: { 订购次数: -1 } }
])
输出:
{ "_id": "键盘", "订购次数": 2 }
{ "_id": "鼠标", "订购次数": 1 }
{ "_id": "显示器", "订购次数": 1 }
{ "_id": "U盘", "订购次数": 1 }
{ "_id": "网线", "订购次数": 1 }
$lookup – 关联查询(类似 JOIN)
$lookup 可以从另一个集合中拉取关联数据,实现左外连接效果。假设我们还有一个顾客集合:
// customers集合
{ "_id": 1, "name": "张三", "level": "VIP" }
{ "_id": 2, "name": "李四", "level": "普通" }
要将订单按顾客名关联等级信息,可以这样写:
db.orders.aggregate([
{ $lookup: {
from: "customers",
localField: "customer",
foreignField: "name",
as: "customerInfo"
}},
{ $unwind: "$customerInfo" },
{ $project: {
"customer": 1,
"amount": 1,
"level": "$customerInfo.level"
}}
])
强大的聚合表达式
除了累加器,聚合管道的各个阶段还可以使用丰富的表达式操作符来处理字段值。它们通常与 $project、$group 或 $addFields 搭配使用。
算术表达式
$add、$subtract、$multiply、$divide、$mod:基本四则运算$ceil、$floor、$round:取整
例:在 $project 中计算折扣金额
{ $project: { 实付金额: { $subtract: ["$amount", "$discount"] } } }
字符串表达式
$concat:拼接字符串$toUpper、$toLower:大小写转换$substrCP:截取子串
日期表达式
$year、$month、$dayOfMonth:提取日期部分$dateToString:日期格式化为字符串
{ $group: {
_id: { 年份: { $year: "$date" }, 月份: { $month: "$date" } },
月度总额: { $sum: "$amount" }
}}
条件表达式
$cond:类似三元运算符[布尔表达式, true值, false值]$ifNull:如果是 null 则返回替代值$switch:多条件分支
示例:根据金额打标签
{ $project: {
customer: 1,
amount: 1,
标签: { $cond: { if: { $gte: ["$amount", 500] }, then: "大额", else: "小额" } }
}}
典型业务分析实战
按日期统计每日销售额
db.orders.aggregate([
{ $group: {
_id: "$date",
日销售额: { $sum: "$amount" },
订单量: { $sum: 1 }
}},
{ $sort: { _id: 1 } }
])
找出消费最高的前两名顾客
db.orders.aggregate([
{ $group: {
_id: "$customer",
总消费: { $sum: "$amount" }
}},
{ $sort: { 总消费: -1 } },
{ $limit: 2 }
])
统计所有商品的总销量,并带上平均售价
借由 $unwind 拆出单品,再组合原订单金额:
db.orders.aggregate([
{ $unwind: "$items" },
{ $group: {
_id: "$items",
销售数量: { $sum: 1 },
总销售额: { $sum: "$amount" }
}},
{ $project: {
商品: "$_id",
销售数量: 1,
平均售价: { $divide: ["$总销售额", "$销售数量"] },
_id: 0
}}
])
优化与注意事项
- 尽早使用
$match和$sort,利用索引减少管道后续处理的数据量。 - 避免在
$group前使用$project丢掉过多字段,除非为了性能刻意减小文档体积。 - 聚合管道的每个阶段处理能力上限为 16MB 的文档,复杂运算时可使用
$limit分批处理或启用磁盘缓存 (allowDiskUse: true)。 - 善用
explain()分析聚合执行计划,优化性能。
总结
MongoDB 的聚合框架是数据处理的利器,它将过滤、变换、分组、排序、关联等操作串成一条管道,让复杂分析变得步骤分明、可读性强。从最简单的 $match+$group 到含有 $lookup 和表达式的复合管道,你可以逐步构建出满足各类业务需求的数据处理流程。
掌握这些基础知识后,进一步可以探索更高级的阶段,如 $facet(多维度分组)、$bucket(时间或数值分桶)以及 Map-Reduce 的替代方案。现在,打开你的 MongoDB 环境,用真实数据实践这些例子吧。