TT Lab
开始
学习 学习路径 课程

MongoDB — 文档数据库的判断

慢的原因多半在索引

在 TT Lab 中继续学习

一句话总结

没有索引的话,文档数据库也会全部扫描一遍,计划里会写成 COLLSCAN。它与 RDB 的 Seq Scan 完全是一回事,对策也相同。

为什么需要它——慢的原因通常只有一个

关于“MongoDB 很慢”的反馈,大多数并不是数据库的问题,而是没有索引的查询。文档只有一万条时没有人察觉。到了一百万条,同样的代码突然变慢,这时人们就会怀疑数据库。

确认的方法与 RDB 相同:询问执行计划。

db.items.find({ name: "빨간 배낭" }).explain()

计划里只需要看两个词。COLLSCAN 表示全部扫描,IXSCAN 表示用了索引。

同时看数字

只看阶段名称,只完成了一半。使用 explain('executionStats'),就能看到实际扫描了多少条。

值 含义
nReturned 实际返回的文档数
totalDocsExamined 为了找到它们而打开的文档数

这两者的比值就是索引的成绩单。如果为了返回一条而打开了一百万条,说明没有索引,或者索引建错了。反过来,两者接近,就说明建得好。

复合索引的关键就是顺序

决定建索引之后,下一个问题是以什么顺序组合。文档数据库的复合索引也是按值排序的结构,所以只能从前面的字段开始使用,而这条规则决定了能用什么、不能用什么。

用 { status: 1, createdAt: -1 } 建立的索引,使用情况如下。

用三种查询扫描先按 status 升序、再按 createdAt 降序排列的复合索引的示意图。用 status 查找时,匹配的行连在一起,只需扫描一个连续区间,加上排序,由于已经是降序,所以也是免费的。只用 createdAt 查找时,匹配的行分散各处,没有连续区间

于是就有了实际工作中的顺序规则:把用相等条件过滤的字段放在前面,把用范围条件过滤或用于排序的字段放在后面。位于范围条件字段之后的字段,不能用于查找,只能用于过滤。

如果排序无法使用索引,成本会悄悄变高。文档数据库在内存中排序,而这个量有上限,超过就会报错失败,或者使用临时文件。即使结果很少,只要排序前的候选很多就会触发,这一点尤其让人吃惊。如果在计划中单独看到了排序阶段,就看看能不能把这个顺序移到索引里。

对数组字段建立的索引性质不同。数组的每个元素都会生成一个条目,所以一个文档会在索引中出现多次。如果对有几百个元素的数组建索引,索引会相应变大,写入也会变慢。而且不能把两个以上的数组字段组合进同一个索引。因为组合的数量会相乘而爆炸。

最后,如果所需的值全部都在索引里,就根本不用打开文档。前面看到的 totalDocsExamined 变成 0 的情况就是这样,对于像列表页面那样只显示几个字段的查询,价值尤其大。

在实际项目中

多建索引并不总是答案。索引会拖慢写入并占用空间。每插入一个文档,所有索引都要更新。

所以有一个顺序。先读执行计划,确认什么慢,然后为那一个查询建索引。“以防万一”建的索引,往往只留下写入成本,却没有让任何查询变快。

是否遵守这个顺序,是询问 SQL 优化经验的面试中分出高下的地方。