这是《MongoDB 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 索引决定查询能否以可控代价执行。MongoDB 支持单字段、复合、多键、文本、地理、哈希、部分、稀疏、TTL 和唯一索引。设计索引时,先看查询条件和排序,再看写入成本和内存占用。
6.1 没有索引会发生什么
集合扫描:
query condition
-> scan collection documents
-> filter one by one
-> return matches
数据量小可以接受,数据量大后会导致:
- 查询延迟高;
- CPU 和 IO 增加;
- 工作集被冷数据污染;
- 阻塞其他请求;
- 复制延迟增加。
6.2 单字段索引
创建:
db.orders.createIndex({ user_id: 1 })
1 表示升序,-1 表示降序。单字段索引支持正反排序。
查询:
db.orders.find({ user_id: "u_1001" }).sort({ created_at: -1 })
该查询若没有复合索引,user_id 索引命中后仍需要内存排序。
6.3 复合索引
创建:
db.orders.createIndex(
{ user_id: 1, created_at: -1, status: 1 },
{ name: "idx_user_time_status"}
)
适用查询:
db.orders.find({ user_id: "u_1001" })
db.orders.find({ user_id: "u_1001", status: "PAID" })
db.orders.find({ user_id: "u_1001" })
.sort({ created_at: -1 })
不适用:
db.orders.find({ status: "PAID" })
复合索引遵循最左前缀原则,字段顺序应结合等值条件、范围条件和排序。
6.4 ESR 原则
推荐顺序:
Equality -> Sort -> Range
示例查询:
db.orders.find({
user_id: "u_1001",
created_at: { $gte: start, $lt: end }
}).sort({ status: 1, created_at: -1 })
索引建议:
db.orders.createIndex({
user_id: 1,
status: 1,
created_at: -1
})
先放等值字段,再放排序字段,最后放范围字段,可以减少扫描和内存排序。
6.5 多键索引
数组字段自动使用多键索引:
db.products.createIndex({ tags: 1 })
查询:
db.products.find({ tags: "hot" })
限制:
- 一个复合索引中通常只能有一个多键字段;
- 数组过大索引开销高;
- 无界数组会放大写入成本;
- 索引条目数量需要关注。
6.6 唯一索引
业务单号:
db.orders.createIndex(
{ order_no: 1 },
{ unique: true }
)
用户名:
db.users.createIndex(
{ email: 1 },
{ unique: true, sparse: true }
)
唯一索引是数据库层幂等和约束的重要手段。应用层校验不能替代唯一约束。
6.7 部分索引
只索引活跃订单:
db.orders.createIndex(
{ user_id: 1, created_at: -1 },
{
name: "idx_active_user_time",
partialFilterExpression: {
status: { $in: ["CREATED", "WAIT_PAY", "PAID"] }
}
}
)
适合:
- 查询永远限定子集;
- 历史数据量大;
- 希望降低索引大小;
- 写入热点明显。
查询条件必须覆盖部分索引条件,否则优化器不能使用。
6.8 TTL 索引
会话过期:
db.sessions.createIndex(
{ expires_at: 1 },
{ expireAfterSeconds: 0 }
)
日志固定保留:
db.login_events.createIndex(
{ created_at: 1 },
{ expireAfterSeconds: 60 * 60 * 24 * 30 }
)
注意:
- TTL 后台线程周期性删除,不是精确到秒;
- 只能用于 Date 或包含 Date 的数组;
- 不适合替代业务级清理审计;
- 删除大量数据仍会影响性能;
- 需要监控删除速率。
6.9 索引查看与删除
查看:
db.orders.getIndexes()
查看大小:
db.orders.stats({ indexDetails: true })
删除:
db.orders.dropIndex("idx_user_time_status")
删除前必须确认没有高频查询依赖该索引,并保留回滚脚本。
6.10 隐藏索引
MongoDB 支持隐藏索引用于验证影响:
db.orders.hideIndex("idx_user_time_status")
恢复:
db.orders.unhideIndex("idx_user_time_status")
隐藏索引仍占存储,但优化器不使用,适合删除前观察。
6.11 索引开销
每个索引都会带来:
- 写入放大;
- 磁盘占用;
- 内存占用;
- checkpoint 压力;
- 维护成本。
索引数量控制建议:
- 每个索引都有明确查询;
- 定期合并重复前缀索引;
- 删除无效索引;
- 用执行计划验证效果;
- 在预生产环境压测。
6.12 常见问题
| 问题 | 原因 |
|---|---|
| 查询没有走索引 | 条件不带最左前缀 |
| 排序内存高 | 排序字段不在索引中 |
| 写入变慢 | 索引过多 |
| 唯一冲突 | 历史重复或空值策略 |
| 部分索引未使用 | 查询条件不满足 |
| TTL 不准时 | 后台删除机制 |
本章小结
索引设计遵循 ESR 原则,等值、排序、范围字段顺序要匹配访问模式。唯一索引用于数据库层约束,TTL、部分索引、隐藏索引是治理成本和变更风险的重要工具。索引不是越多越好,每个索引都必须被查询和监控证明值得存在。
思考题
- 复合索引为什么要遵循最左前缀?
- ESR 原则如何减少内存排序?
- 部分索引为什么能降低成本?
- TTL 索引适合什么场景?
- 删除索引前为什么要隐藏验证?