Zhu.Yang

朱阳的个人博客(公众号:think123)

0%

为什么要有复合索引?

什么是索引

要理解索引,你需要在脑中有个画面,这里建议想象一本食谱,不是普通食谱,而是5000页的厚重食谱,包含各种场合、菜肴和季节的食谱。虽然这个食谱很全,但是它有个缺点就是它是乱序的,第一页可能是鱼香茄子,第3000页是红烧茄子。

这还不是很要紧,关键问题是这本食谱没有索引!

下面是你要问自己的第一个问题:如果没有索引如何在食谱中找到糖醋排骨?唯一的选择是一页一页翻过去,如果它在3892页,你得要翻多少页呀,最坏的情况是它在最后一页,你就得把整本书都翻一遍。

解决办法就是构建一个索引。

我们可以想到多种查找方法,其中食谱名称可以作为一个起点。如果建立一个按食谱名称排列列表,随后是其所在页码,那么就是按食谱名称对书建立索引了。其中的条目可能是这样的:

1
2
3
红烧排骨 : 45
猪肉饺子 : 320
酱萝卜 : 199

现在只要知道食谱的名称就能通过该索引快速找到书中的任意食谱了,如果你只希望通过这种方式来检索食谱,那就已经完事儿了。

但这是不现实的,比方说,你还会希望根据你冰箱里面的食材查找食谱,或者是根据菜肴来进行查找。针对这种情况,你需要更多的索引。

这就产生了第二个问题,只有一个基于食谱名称的索引,如何能找到所有的排骨相关的食谱呢?缺少合适的索引,你仍然需要翻阅整本食谱–5000页。在根据食材或者菜肴进行检索时都是如此。

为此我们需要构建另一个索引,这次是对食材进行索引,在这个索引里面按照字母顺序排列食材,每个食材都指向所有包含它的食谱所在的页码。最基本的食材索引是这样的

1
2
3
牛肉 : 301, 342, 785, 2310, 2456, 4310 ...
山药 : 8, 20, 45, 78, 287, 1295, 4587 ...
猪肉 : 12, 124, 320, 890, 3719, ...

这是你希望的索引吗?是不是很有用?

如果只是需要知道指定食材的食谱清单,这个索引就够用了,但如果还希望在查找时包含任何任意其他与与食谱相关的信息,还是需要进行“扫描”–一旦知道牛肉的页码,你要翻到每一页找到食谱的名字以及确定菜肴类型,虽然这比我们翻阅整本书要好,但是还远远不够。

例如,一周前,你无意间在这本食谱里面发现了一个很棒的鸡肉料理食谱,但是却忘了它的名字,你很想找到它然后做给你心仪的漂亮小姐姐吃。目前为止,有两个索引,一个时食谱名称的索引,另一个是食材的索引。是否能将两者结合起来,找到遗忘的鸡肉食谱呢?

实际上,这是不可能的。如果从食谱名称索引入手,但是却不记得名字,检索这个索引只比翻阅全书好一点点。从食材入手,则要检查一系列页码,但是这些页码无法插入基于食谱名称的索引。因此这种情况下只能使用一个索引,本例中食材的索引要更有用一点。

通常认为一个查询里面要查找两个字段,可以针对它们分离索引。有一个现成的算法:查找每个索引里匹配项的页码,针对同时匹配两个索引的名单扫面它们页码的交集。这样可以减少扫描的总数。一些数据库实现了这个算法,但MongoDB中没有。就算它实现了,使用复合索引来查找两个字段总是会比我刚才描述的算法效率高。请记住每个查询中数据库只会使用一个索引,如果要对多个字段进行查询,请确保有这些字段的符合索引。

那该怎么办?幸好我们有复合索引。

复合索引

目前为止你建立的都是单键索引:它们只是对食谱的一个键进行索引。现在要为整本食谱构建一个新的索引,不同之处这次是要使用两个键。类似的使用多个键的索引成为复合索引(compound index)。

该复合索引依次使用了食材和食谱名称,可以这样来标记它: 材料-食谱,其中的部分内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
猪肉:
猪肉白菜炖粉条: 320
猪肉蛋卷: 3719
猪肉脯: 890

鸡腿:
红烧鸡腿: 82
可乐鸡腿: 3710
土豆焖鸡腿: 2578

西红柿
西红柿炒鸡蛋: 4827
西红柿鸡蛋汤: 2478
西红柿牛腩: 489

这个索引的值对人是显而易见的,现在可以根据食材进行查找,大致定位要找的食谱,哪怕只是记得名称的开头部分。对机器而言同样很有价值,不用扫描该食材的全部食谱名称了。

需要注意的是复合索引的的顺序是很有讲究的,假设将上述索引翻转为 食谱-材料,它能替代我们之前的索引吗?

明显不能!使用新索引,只要知道名称,搜索就一定会定位到一个食谱,书中的一页。如果是要查找含有香蕉食材的猪肉食谱,那就可以确定不存在这个食谱。如果进行翻转之后,我们就必须要知道食谱的名称,在去找食材,但是现实情况往往是我们知道食材却不知道食谱名称。

现在整本食谱有三个索引: 食谱食材食材-食谱,也就是说我们可以安全地去掉食材这个索引了。为什么呢?
因为对某一食材的索引可以使用食材-食谱索引,如果你知道食材,可以便利该复合索引,获得包含它的食谱的页码列表。

总结

本文只是为更进一步认识索引提供一个隐喻,从中可以认识到一些简单的经验法则,如下:

  1. 索引能够显著减少获取文档所需的工作量。没有合适的索引,实现查询的唯一途径就是线性扫描整个文档,直到满足查询条件为止。这通常就是扫描整个集合。
  2. 解析查询时只会使用一个单键索引(or是例外),对于包含多个键(比如食材和食谱)的查询,包含这些键的复合索引能更好地解析查询。

    比如student表,对age和name都建立了索引,如果你查询name=”zhangsan” and age = 20,也只是会使用其中一个索引

  3. 如果有食材-菜谱索引,可以去掉食材索引,也应该这么做。更抽象一点,如果有一个a-b的复合索引,那么仅针对a的索引就是冗余的。但是如果b本身就是一个复合索引(b=c-d),那么同时拥有a-b和a还是很有意义的。
  4. 符合索引里键的顺序也是很重要的。

欢迎关注我的其它发布渠道