这是《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)
优化点:
- 使用投影;
- 使用分页;
- 控制批量大小;
- 大字段拆集合;
- 列表页避免返回详情 HTML;
- 图片只存引用。
网络传输和应用反序列化同样是查询成本。
11.3 优化过滤条件
避免低选择性条件:
db.orders.find({ status: { $ne: "CANCELLED" } })
改为正向状态:
db.orders.find({
status: { $in: ["CREATED", "WAIT_PAY", "PAID"] },
user_id: "u_1001"
})
原则:
- 等值条件放前面;
- 高选择性字段参与索引;
- 范围控制时间窗口;
- 避免超长
$in; - 不让用户提交任意复杂查询。
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 } } }
])
规则:
$match和$limit前移;$project减少字段;$unwind后立即过滤;$lookup确保外键有索引;- 避免无限
$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
优化:
- 减少索引总量;
- 控制文档大小;
- 投影减少读取;
- 冷数据归档;
- 分片分散热点;
- 增加内存;
- 分析任务移到专用系统。
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 和资源。执行计划是判断依据,慢查询日志和监控是发现入口,预生产压测是上线保障。
思考题
- 为什么投影也能提升性能?
- 大
skip为什么慢? $match前移为什么重要?- 如何判断工作集超过内存?
- 索引过多的代价是什么?