天天看点

MongoDB explain 实战-看看你的 index 是真有在做事,还是占空间而已

作者:墨也的笔
MongoDB explain 实战-看看你的 index 是真有在做事,还是占空间而已

再使用 MongoDB 时,如果完全没有帮 collection 建立 index,那在搜寻时就会需要把整个 collection 翻过来一个一个找过。

就像你到小北百货想要买一个「键盘」、一个「鼠标」,如果店家没有帮你分类成「计算机外围」、「水电材料」、「卫浴用品」几大类,加上键盘跟鼠标也不见得放在一起,那可能要逛完一整圈才能找到你要的东西。是还好逛一圈小百百货也没多久,偶尔去逛一下还能发现一些便宜的好物XD。

但数据库就不一样了,尤其大数据的时代,连小公司都有几百万笔的资料。这时如果还一个一个慢慢找简直就是旷日费时,因此怎么让 DB 吃到 index 是一件非常重要的事。

图解 Index

在讲 explain 之前先来说说 index 大概长成什么样子。假如果你今天开一间五金行,要用数据库来记录各个商品的库存(inventory)状况,那你的 collection 里面就会有键盘、鼠标、电风扇等等商品的价格跟数量

MongoDB explain 实战-看看你的 index 是真有在做事,还是占空间而已

这时如果根据价格帮他们建 index,让商品从最便宜排到最贵,那 MongoDB 并不会去修改资料在硬盘中的位置,而是会另外建一个 price 排序过后的清单并用指标指向资料位置。如此一来,当你想要找价格 699 元的东西时,Mongo 就会很快的从那个清单来找,再藉由指标拿到真实资料

MongoDB explain 实战-看看你的 index 是真有在做事,还是占空间而已

因为 index 只是另外建一个清单,所以想在同一个 collection 内建多个 index 也是没问题的唷!譬如说我可以同时帮 price 跟 quantity 建 index,这样再根据价格或数量做 query 时就都有 index 可以用

认识 explain

既然加 index 可以加速 query,那是不是拼命帮各个栏位加 index 就好?也不是这样,重点在于你加的 index 有没有被 query planner 用到。加了太多没路用的 index 只不过是占空间,而且还会拖慢写入的速度。

我这边有准备一个GitHub - LarryLuTW/mongo-explain-demo,里面已经塞了一千笔资料,只要跟着 README 用 Docker 跑起来就可以一起玩 explain 囉(我很少这么认真写 README 拜托大家去跑一下XD)

先来看看完全没 index 的情况下,如果想找到价钱 699 的商品,Mongo 会怎么做搜寻。方法很简单,只要在 find({ price:699 }) 后面加上explain() 就可以了(跑出来会有一大串,我们先看其中的 queryPlanner.winningPlan)

MongoDB explain 实战-看看你的 index 是真有在做事,还是占空间而已

这边有个关键stage: 'COLLSCAN',意思是这次 query 是把整个 collection 都找(scan)过一遍,可想而知效率一定非常的差。

如果想看更详细的执行情况的话,可以带参数explain("executionStats"),就可以看到这次 query 的过程总共检查了 1000 笔资料(就全部啦XD),最后符合条件的资料却只有 1 笔,好像很可怜

MongoDB explain 实战-看看你的 index 是真有在做事,还是占空间而已

加上 index

那要怎么解决命中率只有千分之一的窘境呢,那就是加一个{ price: 1 } index,意思是建一个 index,让他根据 price 从小排到大

MongoDB explain 实战-看看你的 index 是真有在做事,还是占空间而已

有了 index 后马上来看看 explain,果然从 COLLSCAN 变成IXSCAN+FETCH 了。注意当有多个步骤(stage)时要从内层往外看,所以是先做 IXSCAN 再做 FETCH,意思是先从 price_1 index 中找到 699(Index Scan),再去把那些资料抓出来(Fetch)。因此我们刚新增的 price_1 是有帮助的。

MongoDB explain 实战-看看你的 index 是真有在做事,还是占空间而已

Mongo 是怎么选择 query plan 的呢?

到这边应该都对 explain 有点概念了,接着来讲一个比较复杂的例子,看 Mongo 在多个 index 时,是怎么选择「他所认为的最佳的 query plain」

MongoDB explain 实战-看看你的 index 是真有在做事,还是占空间而已

现在 price 跟 quantity 栏位上都有 index。先用脑袋瓜想一下,如果我今天想要补库存,要找「库存量只剩一个,且价钱低于一万的商品」,那怎么使用 index 来找会最快呢?这边有两个方案:

MongoDB explain 实战-看看你的 index 是真有在做事,还是占空间而已

方案一:先利用 quantity index 快速找到quantity == 1 的那些商品(假设有 200 个好了),再从 200 个中一个一个检查,找出price < 10000 的商品

方案二:先利用 price index 快速找到price < 10000 的商品(只是家五金行,一万块以下的商品会超多,所以假设有 800 个),再从 800 个中一个一个检查,找出quantity == 1 的商品

比较一下,方案一光是第一步的 quantity == 1 就可以筛选掉不少资料,再从中找 price < 10000 应该很快,CPU 总共只需要做 200 多次比较;而方案二的第一步 price < 10000 可以筛选掉的资料很少,加上第二步可能要做 800 多次的比较。所以没意外的话应该是方案一比较好

但这毕竟是我们人工判断的结果,之所以能这样判断是因为我们知道这是一家五金行的资料、也知道他的资料特性(一万块以下的商品会超多)。但对数据库而言里面就只是一堆资料,所以 MongoDB 自己有一套方法来选出最佳方案。

不知道哪个方法快?那就跑跑看吧!

对 MongoDB 而言,因为不知道里面的资料长什么样子,所以他会直接把各个可能的 plan 都试跑一下,看哪个 plan 最先回传 101 笔结果,就会被选出来当 winning plan

以这个例子来说,我们可以用 explain("allPlansExecution") 来看各个 plan 执行的状况(因为太详细了,直接给大家看重点)

从下图可以看到当方案一(先用 quantity index)抓到前 101 笔资料时,方案二(先用 price index )才刚抓到 15 笔资料而已,所以正如我们预期的,方案一确实比方案二快上许多

MongoDB explain 实战-看看你的 index 是真有在做事,还是占空间而已

而 MongoDB 其实也不会每次都做方案之间的比较,他只要比过一次就会把结果 cache 起来,避免每次 query 都浪费在做一样的事情。

所以如果你在 production 上加了 index 之后 MongoDB 没有马上使用新的 index,那也不代表他不好,只是可能要过个几天才会被 MongoDB 用上

强迫推销 index

虽然 MongoDB 大部分情况下都会选择最佳方案,但在极少数的情况下也可能会选错。所以如果你加了一个你觉得「超级好用、一定要用、不用会出大事」的 index,但 MongoDB 却迟迟没有用上,那就可以用 hint(index) 来强迫推销 MongoDB 一定要使用你的 index

以刚刚的例子来说,MongoDB 认为最佳方案是优先使用quantity_1,跑出来的结果是:总共需要检查 148 个 document 才能得到结果

MongoDB explain 实战-看看你的 index 是真有在做事,还是占空间而已

但如果我觉得price_1 才是真正对他好的 index,那就可以在 query 时加上hint("price_1") 提醒(强迫)他用这个 index 来做搜寻,然后看看结果是不是跟你想的一样好。

MongoDB explain 实战-看看你的 index 是真有在做事,还是占空间而已

以这个例子来说,强迫他使用price_1 的结果就是totalDocsExamined 从 148 变成 988,速度直接慢了好几倍,一点帮助都没有

总结

到这边大家对于 explain 的使用方式都有些概念了,透过 explain,你可以不断检验 index 是不是真的适合你的应用,避免自己加了一堆「感觉有用」的 index,但其实只是多占空间而已。

受限于篇幅,今天讲到的 index 都是针对某个栏位的 Single Field Indexes — MongoDB Manual 而已,之后有机会再来讲怎么针对各种应用场景,客制化最适合的 Compound Indexes — MongoDB Manual 及 Partial Indexes — MongoDB Manual,让你 query 的速度更上一层楼~