MongoDBNotes

第 11 章:查询优化

zjc 于 2026-01-11 发布

这是《MongoDB 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 查询优化是把业务访问模式、索引设计、执行计划和资源限制统一起来的过程。目标不是让每条查询都极快,而是让核心链路在真实数据量和并发下稳定满足 SLA。

11.1 优化流程

collect slow queries
  -> review query semantics
  -> run explain executionStats
  -> inspect indexes and schema
  -> reduce scanned data
  -> remove blocking stages
  -> verify in staging
  -> monitor after release

先确认业务真的需要该查询,再优化实现。

11.2 减少返回数据

只取必要字段:

db.orders.find(
  { user_id: "u_1001", status: "PAID" },
  { order_no: 1, amount: 1, created_at: 1 }
).limit(20)

优化点:

  1. 使用投影;
  2. 使用分页;
  3. 控制批量大小;
  4. 大字段拆集合;
  5. 列表页避免返回详情 HTML;
  6. 图片只存引用。

网络传输和应用反序列化同样是查询成本。

11.3 优化过滤条件

避免低选择性条件:

db.orders.find({ status: { $ne: "CANCELLED" } })

改为正向状态:

db.orders.find({
  status: { $in: ["CREATED", "WAIT_PAY", "PAID"] },
  user_id: "u_1001"
})

原则:

  1. 等值条件放前面;
  2. 高选择性字段参与索引;
  3. 范围控制时间窗口;
  4. 避免超长 $in
  5. 不让用户提交任意复杂查询。

11.4 优化排序与分页

索引:

db.orders.createIndex({ user_id: 1, created_at: -1, _id: -1 })

游标分页:

db.orders.find({
  user_id: "u_1001",
  $or: [
    { created_at: { $lt: lastCreatedAt } },
    { created_at: lastCreatedAt, _id: { $lt: lastId } }
  ]
}).sort({ created_at: -1, _id: -1 }).limit(20)

skip 会扫描并丢弃数据,深分页应改为游标或条件分页。

11.5 优化聚合管道

对比:

// bad
db.orders.aggregate([
  { $group: { _id: "$user_id", total: { $sum: "$amount" } } },
  { $match: { total: { $gt: 1000 } } }
])

改进:

db.orders.aggregate([
  { $match: {
      status: "PAID",
      created_at: { $gte: new Date("2026-08-01T00:00:00Z") }
  }},
  { $group: { _id: "$user_id", total: { $sum: "$amount" } } },
  { $match: { total: { $gt: 1000 } } }
])

规则:

  1. $match$limit 前移;
  2. $project 减少字段;
  3. $unwind 后立即过滤;
  4. $lookup 确保外键有索引;
  5. 避免无限 $push

11.6 索引优化

重复前缀:

{ user_id: 1 }
{ user_id: 1, created_at: -1 }

通常后者可以覆盖前者的一部分场景。删除前要确认剩余查询和写入收益。

多字段查询:

db.orders.find({
  user_id: "u_1001",
  status: "PAID",
  created_at: { $gte: start, $lt: end }
}).sort({ created_at: -1 })

索引:

db.orders.createIndex({ user_id: 1, status: 1, created_at: -1 })

11.7 Schema 优化

常见问题与调整:

问题 优化
文档过大 拆分低频大字段
无界数组 拆集合并分页
高频计数 使用 $inc 或独立计数集合
列表读大文档 投影和摘要
高频 $lookup 冗余必要字段
历史数据混存 冷热分离

如果索引优化收益有限,通常需要回到数据模型。

11.8 工作集优化

工作集是常用数据和索引的集合。如果工作集明显大于内存,会产生更多磁盘 IO。

观察:

memory resident
cache hit ratio
disk read / write
query latency
eviction

优化:

  1. 减少索引总量;
  2. 控制文档大小;
  3. 投影减少读取;
  4. 冷数据归档;
  5. 分片分散热点;
  6. 增加内存;
  7. 分析任务移到专用系统。

11.9 慢查询治理

设置 Profiler:

db.setProfilingLevel(1, { slowms: 100, sampleRate: 1 })

查看:

db.system.profile.find().sort({ ts: -1 }).limit(10)

关闭:

db.setProfilingLevel(0)

Profiler 有额外开销,生产应控制采样率或短期开启。

11.10 常见反模式

反模式 后果
请求全量再应用过滤 数据库和网络浪费
大 skip 分页 深分页变慢
无索引排序 内存排序
复杂正则搜索 CPU 升高
索引过多 写入变慢
聚合做大数据分析 影响在线库
每次查询重建连接 延迟增加

本章小结

查询优化的第一步是减少扫描和返回数据,第二步是让排序和过滤匹配索引,第三步才是调整 Schema 和资源。执行计划是判断依据,慢查询日志和监控是发现入口,预生产压测是上线保障。

思考题

  1. 为什么投影也能提升性能?
  2. skip 为什么慢?
  3. $match 前移为什么重要?
  4. 如何判断工作集超过内存?
  5. 索引过多的代价是什么?