索引优化
复合/文本/地理索引。
索引优化
这一课你将能做到
- 创建单字段、复合、多键、文本、TTL、唯一索引,并说明各自适用场景
- 用 explain("executionStats") 读懂执行计划,判断查询是否走索引、是否高效
- 用 ESR 法则(Equality → Sort → Range)设计复合索引,识别覆盖查询
为什么要学
没索引的查询是全表扫描(COLLSCAN):100 万条提交记录查一个用户,MongoDB 要逐条读 100 万条。有索引后,B+ 树几次磁盘跳转就定位,毫秒级返回。线上接口从 5 秒降到 5 毫秒,往往只是差一条 createIndex。
但索引不是免费的:每个索引都让写入变慢(要维护树)、占内存和磁盘。这一课教你「给对的查询建对的索引」,而不是见查询就建。
先建立图像
复合索引 { userId: 1, submittedAt: -1 } 的 B+ 树(逻辑视图):
userId=1024 ── submittedAt=10-04
── submittedAt=10-02
── submittedAt=10-01
userId=2048 ── submittedAt=10-05
── submittedAt=10-03
索引条目按 (userId 升序, submittedAt 降序) 整体有序。
能服务的查询:
✅ {userId: 1024} 等值
✅ {userId: 1024} sort {submittedAt: -1} 等值+排序(索引顺序即结果顺序)
❌ {submittedAt: ...} 单独查 跳过了前缀 userId,用不上
这就是最左前缀:复合索引必须从第一个字段开始连续使用。
explain 关键指标:
stage: COLLSCAN ← 全表扫描,危险信号
stage: IXSCAN ← 走索引,好
totalDocsExamined: 扫描的文档数 ← 越小越好
nReturned: 返回数
理想:docsExamined ≈ nReturned(扫的都是要的)
糟糕:docsExamined = 1000000, nReturned = 3
跟着做
准备数据(量大一点才有感觉):
use codeforge
db.submissions.drop()
// 插入 5 万条模拟提交
const bulk = []
for (let i = 0; i < 50000; i++) {
bulk.push({
userId: 1000 + (i % 500),
lessonSlug: "mdbrm-" + (i % 12),
status: i % 3 === 0 ? "failed" : "accepted",
score: i % 101,
submittedAt: new Date(Date.now() - i * 60000)
})
}
db.submissions.insertMany(bulk)
步骤 1:看没索引时的惨状
db.submissions.find({ userId: 1024 }).explain("executionStats")
看 totalDocsExamined: 50000、stage: "COLLSCAN"。
步骤 2:单字段索引
db.submissions.createIndex({ userId: 1 })
db.submissions.find({ userId: 1024 }).explain("executionStats")
stage 变成 IXSCAN,docsExamined 降到 100 左右。
步骤 3:复合索引与 ESR 法则
目标查询:「某用户的提交,按时间倒序,且分数大于 60」。
db.submissions.createIndex({ userId: 1, submittedAt: -1, score: 1 })
ESR 法则:。这里 userId 是等值、submittedAt 是排序、score 是范围过滤——顺序正好。