MongoDB 基础与聚合:文档模型与管道操作

FreeGuideOnline 最新 2026-06-13

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 环境,用真实数据实践这些例子吧。