MongoDB 索引与查询性能
单字段/复合/唯一/稀疏索引的创建与选择,用 explain 读懂查询计划,避免全表扫描。
0. 一句话理解
索引就是数据库给字段做的”目录”:没有目录就一页页翻(全表扫描),有了目录就能直接翻到目标页。索引让查询变快,但每次写入都要维护目录,所以不能乱建。
1. 查看与创建索引
// 查看现有索引
db.students.getIndexes()
// 给 age 建普通索引(1 升序,-1 降序,对等值查询无区别)
db.students.createIndex({ age: 1 })
// 唯一索引:name 不允许重复
db.students.createIndex({ name: 1 }, { unique: true })
// 复合索引:先按 class,再按 age
db.students.createIndex({ class: 1, age: -1 })
讲解:
getIndexes()会看到默认的_id索引——主键索引由 MongoDB 自动创建。createIndex({ age: 1 })的1/-1只影响排序方向,等值查询两种都能用。unique: true用来保证字段唯一,插入重复值会报错,适合用户名、订单号。- 复合索引的字段顺序很重要:
{ class: 1, age: -1 }能服务”按班级查”和”按班级+年龄查”,但单独按年龄查用不上它。
2. 用 explain 读查询计划
db.students.find({ age: { $gt: 18 } }).explain("executionStats")
讲解:
explain("executionStats")返回查询计划,重点看三个字段:winningPlan.stage:COLLSCAN表示全表扫描,IXSCAN表示走了索引;totalDocsExamined:实际翻看了多少条文档,越接近返回条数越好;executionTimeMillis:本次查询耗时。
- 练习:给
age建索引前后各 explain 一次,对比COLLSCAN与IXSCAN的totalDocsExamined。
3. 索引的代价与取舍
| 情况 | 建议 |
|---|---|
| 查询条件字段 | 建索引 |
| 排序字段 | 建索引(避免内存排序) |
| 区分度低的字段(如性别) | 不建索引,扫描率太高 |
| 几乎不查的字段 | 不建索引 |
| 写入密集的集合 | 控制索引数量,写入时每条索引都要更新 |
| 超大文本字段 | 用文本索引(text)而非普通索引 |
// 稀疏索引:只为存在该字段的文档建索引
db.students.createIndex({ phone: 1 }, { sparse: true })
// TTL 索引:字段超过 3600 秒自动删除文档(日志、会话场景)
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })
讲解:
sparse: true适合”很多文档没有这个字段”的情况,减少索引体积。expireAfterSeconds是 MongoDB 的定时过期机制,后台约每 60 秒清理一次,适合验证码、临时会话。
4. 查询性能自查清单
- 先用
explain确认是否COLLSCAN; - 检查查询条件与排序是否被同一个复合索引覆盖;
- 避免对索引字段使用
$where、正则前缀以外的表达式; - 大结果集用游标分批处理,不要一次
find({})全量拉取; - 生产环境用 MongoDB Compass 的索引建议或
$indexStats观察索引使用率。
5. 动手试试
- 造 10 万条测试数据(可用
insertMany+ 循环),对比age有/无索引时的查询耗时。 - 给
{ class: 1, age: -1 }建复合索引,测试三种查询:只按 class、按 class+age、只按 age,观察哪个没走索引。 - 给订单集合加 TTL 索引,插入一条
createdAt为过去时间的文档,等待清理(可把expireAfterSeconds设小)。
6. 一句话记住
查询慢先跑
explain:看到COLLSCAN就建索引;索引不是越多越好,查询多、区分度高的字段才值得建。