Skip to content

MongoDB 索引、查询与聚合

这篇笔记专门整理 MongoDB 里最影响性能表现的一条主线:索引设计 -> 查询路径 -> 聚合管道 -> 分页与治理

如果说文档模型决定了数据怎么放,那么索引和查询路径决定的就是:这些数据到底能不能被稳定、高效地取出来。


1. 为什么 MongoDB 也同样依赖索引

MongoDB 虽然不是关系型数据库,但只要进入真实业务场景,性能问题依然会落到类似的几个核心点上:

  1. 是否需要扫描大量文档
  2. 查询条件和排序是否命中索引
  3. 返回字段是否过多
  4. 聚合阶段是否过早放大数据量

所以不能因为 MongoDB 结构灵活,就把索引设计当成次要问题。

更具体一点:MongoDB 的灵活性解决的是建模问题,索引设计解决的是查询效率问题。


2. 🌟 常见索引类型

MongoDB 常见的索引类型包括:

类型适合什么场景需要特别注意什么
单字段索引单一高频过滤字段只能覆盖有限查询模式
复合索引多条件过滤、排序字段顺序非常重要
唯一索引业务唯一约束写入时会做唯一性校验
文本索引文本搜索更适合基础全文检索,不等于搜索引擎
地理空间索引地图、坐标、附近搜索需要配合地理查询操作符
TTL 索引自动过期数据适合日志、会话、缓存类数据
多键索引数组字段查询数组展开后要注意索引行为

其中最常用、也最值得优先掌握的,还是:

  1. 单字段索引
  2. 复合索引
  3. 唯一索引
  4. TTL 索引

3. 🌟 复合索引为什么最容易设计错

MongoDB 中大量真实查询都不是只按一个字段过滤,因此复合索引往往比单字段索引更常见。

例如:

js
db.orders.find({
  userId: 1001,
  status: "PAID"
}).sort({ createTime: -1 })

这种查询是否稳定,关键往往不在于“有没有索引”,而在于:索引字段顺序是否贴合过滤和排序路径。

例如下面这个复合索引:

js
{ userId: 1, status: 1, createTime: -1 }

它通常就比三个单列索引更贴近这条查询。

设计复合索引时,可以优看这几件事:

  1. 高频过滤字段是谁
  2. 排序字段是谁
  3. 哪些条件经常一起出现
  4. 哪些字段区分度更高

如果索引顺序和真实查询路径不一致,即使建了索引,也可能仍然出现扫描量过大或者排序无法充分利用索引的情况。


4. 查询路径怎么判断是否合理

MongoDB 查询是否高效,核心看的是:

  1. 过滤条件是否先缩小了范围
  2. 排序是否沿着索引顺序执行
  3. 返回结果是否只取了需要的字段
  4. 是否避免了大范围跳页和无谓扫描

4.1 先过滤,再考虑排序和返回

一般来说,查询设计可以按这个顺序思考:

  1. 哪些字段决定过滤范围
  2. 结果需要按什么字段排序
  3. 最终到底需要返回哪些字段

如果一开始就 find({}) 再去排序、再去聚合,性能成本通常会很快放大。

4.2 Projection 也值得重视

例如:

js
db.users.find(
  { status: "ACTIVE" },
  { name: 1, email: 1, _id: 0 }
)

只返回必要字段,不只是“看起来整洁”,它还能减少网络传输和对象反序列化成本。

4.3 深分页仍然是性能问题

像下面这种写法:

js
db.orders.find({ status: "PAID" }).skip(100000).limit(20)

在数据量大时仍然会有明显代价,因为数据库往往还是要跳过前面大量结果。

更稳妥的做法通常是:

  1. 用有序字段做游标分页
  2. 用上一次结果的边界值继续翻页
  3. 尽量避免无限深页码跳转

5. 🌟 聚合管道应该怎么理解

MongoDB 的 Aggregation Pipeline 很像一条数据处理流水线。常见阶段包括:

  1. $match
  2. $project
  3. $group
  4. $sort
  5. $limit
  6. $lookup
  7. $unwind

可以把它理解成:先尽量缩小输入,再做变形、分组、排序和关联。

一个很常见的统计例子:

js
db.orders.aggregate([
  { $match: { status: "PAID" } },
  { $group: { _id: "$userId", totalAmount: { $sum: "$amount" } } },
  { $sort: { totalAmount: -1 } },
  { $limit: 10 }
])

这条链路的性能关键,通常不是 $group 语法会不会写,而是:

  1. $match 有没有尽量提前
  2. 输入数据是不是已经足够小
  3. 排序和分组是不是处理了过大的中间结果

6. $lookup 能用,但不要把它当成天然的多表方案

MongoDB 支持 $lookup 做关联,但这不代表它就应该被默认当成“和关系型数据库一样自然的多表联查引擎”。

更稳妥的理解是:

  1. $lookup 适合有限度地补充关联能力
  2. 如果大量核心查询都依赖复杂 $lookup,往往说明数据模型可能不够贴合文档思路
  3. 文档型数据库更强调把高频一起读取的数据尽量聚合在一起

所以看到 $lookup 时,更值得先问的是:这块数据到底应该引用,还是本来就应该嵌入到主文档里。


7. 怎么排查 MongoDB 查询慢

排查时可以优看这些方向:

  1. 是否命中了合适索引
  2. 查询条件是否过宽
  3. 排序是否借助了索引
  4. 是否返回了过多字段
  5. 是否用了高成本的 $lookup$unwind$group
  6. 是否出现了深分页

一个比较实用的排查顺序是:

  1. 看高频查询长什么样
  2. 再看索引是否贴合这些查询
  3. 再看聚合链路是不是把过滤放晚了
  4. 最后再看是否需要改文档模型

也就是说,MongoDB 查询慢不一定先去怪“数据库不够快”,很多时候真正需要调整的是:

数据结构和查询路径之间有没有对齐。


8. 一份实用的索引与查询检查清单

可以优先检查这些问题:

  1. 高频查询有没有对应索引
  2. 复合索引顺序是否贴合过滤和排序
  3. 是否有大量只靠 skip 的深分页
  4. 聚合管道是否把 $match 提前
  5. $lookup 是必要补充,还是已经变成主查询路径
  6. 是否只返回必要字段
  7. TTL、唯一索引、文本索引是否各自用在合适场景

MongoDB 的查询优化,本质上不是语法技巧,而是索引、数据分布和查询路径的共同设计。

基于 VitePress 构建的个人技术笔记。