慢的原因多半在索引
一句话总结
没有索引的话,文档数据库也会全部扫描一遍,计划里会写成 COLLSCAN。它与 RDB 的 Seq Scan 完全是一回事,对策也相同。
为什么需要它——慢的原因通常只有一个
关于“MongoDB 很慢”的反馈,大多数并不是数据库的问题,而是没有索引的查询。文档只有一万条时没有人察觉。到了一百万条,同样的代码突然变慢,这时人们就会怀疑数据库。
确认的方法与 RDB 相同:询问执行计划。
db.items.find({ name: "빨간 배낭" }).explain()
计划里只需要看两个词。COLLSCAN 表示全部扫描,IXSCAN 表示用了索引。
同时看数字
只看阶段名称,只完成了一半。使用 explain('executionStats'),就能看到实际扫描了多少条。
| 值 | 含义 |
|---|---|
nReturned |
实际返回的文档数 |
totalDocsExamined |
为了找到它们而打开的文档数 |
这两者的比值就是索引的成绩单。如果为了返回一条而打开了一百万条,说明没有索引,或者索引建错了。反过来,两者接近,就说明建得好。
复合索引的关键就是顺序
决定建索引之后,下一个问题是以什么顺序组合。文档数据库的复合索引也是按值排序的结构,所以只能从前面的字段开始使用,而这条规则决定了能用什么、不能用什么。
用 { status: 1, createdAt: -1 } 建立的索引,使用情况如下。
- 只用
status查找——可以使用。 - 用
status查找,并按createdAt排序——可以使用。排序是免费的。 - 只用
createdAt查找——无法使用。因为前面的部分是空的。
于是就有了实际工作中的顺序规则:把用相等条件过滤的字段放在前面,把用范围条件过滤或用于排序的字段放在后面。位于范围条件字段之后的字段,不能用于查找,只能用于过滤。
如果排序无法使用索引,成本会悄悄变高。文档数据库在内存中排序,而这个量有上限,超过就会报错失败,或者使用临时文件。即使结果很少,只要排序前的候选很多就会触发,这一点尤其让人吃惊。如果在计划中单独看到了排序阶段,就看看能不能把这个顺序移到索引里。
对数组字段建立的索引性质不同。数组的每个元素都会生成一个条目,所以一个文档会在索引中出现多次。如果对有几百个元素的数组建索引,索引会相应变大,写入也会变慢。而且不能把两个以上的数组字段组合进同一个索引。因为组合的数量会相乘而爆炸。
最后,如果所需的值全部都在索引里,就根本不用打开文档。前面看到的 totalDocsExamined 变成 0 的情况就是这样,对于像列表页面那样只显示几个字段的查询,价值尤其大。
在实际项目中
多建索引并不总是答案。索引会拖慢写入并占用空间。每插入一个文档,所有索引都要更新。
所以有一个顺序。先读执行计划,确认什么慢,然后为那一个查询建索引。“以防万一”建的索引,往往只留下写入成本,却没有让任何查询变快。
是否遵守这个顺序,是询问 SQL 优化经验的面试中分出高下的地方。