三 索引
前一篇讲了一些查询的一些基础操作和一些骚操作。本篇讲索引。由于笔者未曾好好地学习过一次数据库,所以对索引这一概念其实知之甚少,在本篇会比较细致地学习一遍索引这一概念。
简介
创建索引:
1 | db.data.ensureIndex({'field': 1}) |
为啥要有索引捏
自我理解是使用索引地键对所有文档进行一次排序。这要求数据库维护一个已排序的列表,其中的元素应该包括两个:indexValue, _id。当进行查询的时候数据库可以用各种简单的方法快速定位到索引元素,找到它的_id。
同一个集合,同样的索引只能创建一次,重复尝试创建也没用。
对某个键创建了索引,譬如对x键创建了索引,只会对查询条件里只有x的查询有用,若查询条件中包括了其他条件,比如{‘x’: xx, ‘y’: yy},那么这索引在此次查询便是无用的。
当做一个没有索引的查询的时候,数据需要做一次扫描全表的操作,在集合非常大的时候这个操作会非常耗时。建立索引带来的速度提升不言而喻,然而索引也不是越多越好。创建索引的缺点就是每次增删改的时候都会产生额外的开销,那是因为可能要更改索引列表中的排序。
除了有益于快速查询之外,索引对大剂量排序也有好处。设想如果没有索引,那么对数据排序就需要先把数据都提取到内存中,再排序输出到客户端。当数据很多的时候,就有爆内存的风险了。但是如果有索引,数据库就可以按顺序提取数据。
每个集合默认最多能有64个索引。
另外,不一定使用索引查询就一定快过表扫描。当要返回的结果数量超过集合中文档数量的一半的时候,表扫描是要比索引查询快的。所以查询某个键是否存在,或者某个布尔值真假的时候,没有必要使用索引。
创建索引
创建索引有可能需要数分钟,如果调用ensureIndex之后没有立即返回,可以在另一个shell中执行db.currentOp()来看当前进度。
复合索引
可以指定多个索引,比如{'x': 1, 'y': -1}
。有三种情况可以使用这个索引。很好理解,毕竟索引是按照x和y一起来排序的。
db.user.find({'x': 2}).sort({'y': -1})
找出x为2这个操作非常高效,而对于所有同为一个x的文档来说,y也是排好序的,所以不需要另外排序,不需要多余操作,也非常高效。db.user.find({'x': {'lte': 10, 'gte': 5}})
db.user.find({'x': {'lte': 10, 'gte': 5}}).sort({'y': 1})
这个操作分为两步。第一步先筛选出按照x大小排序的符合条件的文档。但是这些文档是按照x来排序的,并没有根据y的大小来排序,所以还需要第二步,先把文档提取到内存,再进行一次排序。
cautious: 如果索引是{'y': 1, 'x': -1}
的话,可以想象得出这个操作就只需要做find操作,而不需要后面的sort操作了。
总的来说就是加入索引是一个复合索引,那么只对第一个键进行单纯的查询操作是很快的,但是如果查询完还要再根据这个键之外的键进行排序,那么就还需要花费时间来做排序操作。
想要提高操作的效率,就要尽量避免sort(我的意思就是尽量把经常要sort的键拿来当索引中的第一个键),而多做find。毕竟find的时间复杂度是$n$,而排序的时间复杂度就不好说了。
注意,相互反转的索引是等价的。相互反转的定义是每个方向都乘以-1.
隐式索引
很容易得出结论创建索引{'a': 1, 'b': 1, ... , 'z'}
也就相当于创建了{'a': 1}
、{'a': 1, 'b': 1}
等一系列索引。
索引的数据结构
索引的数据结构大概是树,最小的元素是最左边的叶子,最大的元素是最右边的叶子。
如何优化索引
如果索引里面只包含一个键,那么升序降序是无所谓的。但是如果包含多个键,就要考虑每个键的升序降序问题。数据库会有限对先点定义的键进行排序。如
1 | db.data.ensureIndex({'field1': 1, 'field2': -1}) |
数据库会先根据field1进行升序排序,然后对每个field1相同的子集根据field2进行降序排序。
针对那种包含几个键的索引,尽可能地把经常新增地作为第一个排序依据。
例如一个记录朋友圈的数据库有user
、date
二键,前者为用户id,后者为朋友圈发布日期。由于发布朋友圈这个操作非常频繁,所以理应把索引设置成{'date': -1, 'user': 1}
而非{'user': 1, 'date': -1}
。当设置成{'date': -1, 'user': 1}
之后,每次插入新的记录(时间永远都是往前走),就无需在索引中间插入数据(which is a waste),而是在索引的两端插入了。
假设操作先要find出x,再用y做范围筛选,那么索引应该先设定x,再设定y。
$操作是怎么使用索引的
有几个操作是根本不能使用索引的,比如$where
和$exsit
。
$ne
操作对索引的使用也不是那么高效率。毕竟想要找出不等于的,总得遍历全集才能确认不是?
一般而言,MongoDB一次查询只会使用一个索引。假设建立了两个索引{'x': 1}
和{'y': 1}
,那么执行查询条件{'x': 123, 'y': 456}
的时候,很有可能是先用x索引找出一个子集,再在这个子集上找符合y条件的结果。但是如果用的是or查询,由于or查询实际上是把多个查询条件查出来的结果做一次并集,所以or会执行和查询条件一样多次的查询。比如如果条件为{'$or': ['x': 123, 'y': 456]}
,那么实际上是以x为索引搜出结果,再以y为索引搜出结果,再合并。
其他特性
索引可以是内嵌文档中的键,比如{'comment.date': 1}
。
在建立索引的时候可以给它起个可爱又帅气的名称,如果执意不肯给它起名字,数据库就会把键连起来作为它的名称(比如field1_field2_fieldk
),如果这个名字太长超过了限制,就会创建索引失败。执行创建索引的操作之后最好用getLastError来看是否成功创建了索引。
1 | db.data.ensureIndex({'field1': 1, 'field2': -1}, {'name': '屎蛋'}) |
查询优化器
使用哪个索引?
当有一个索引能精确匹配搜索的时候,查询优化器会有限用它。如果有几个同时匹配,MongoDB会并行这些查询,哪个先返回100个结果就会被保留并缓存下来再接下来的查询中使用,其他的查询计划会被中止。
被缓存的查询计划在接下来都会被使用,直到集合数据发生了比较大的变化或者查询超过1000次之后,或者建立了新的索引之后,查询优化其会重新评估。
explain中的allPlans字段显示了本次查询尝试过的所有查询计划。
不适用索引的场合
使用索引需要进行两次查找:
- 查找索引条目
- 根据索引指针查找相应文档
当结果集合在原集合中占的比例越大,就越不适合用索引。最极端的情况是要求返回整份文档,此时使用索引所需要的操作会比表扫描多一倍。
索引通常适合在集合比较大、文档比较大或者选择性查询的时候使用。
索引类型
唯一索引
建立这个索引的目的是防止插入相同的数据。
1 | db.users.ensureIndex({'username': 1}, {'unique': true}) |
插入重复键的时候会抛异常,这会影响效率。所以不能用这个功能来过滤,它只能应对偶现的不太能解决的问题。
_id是一个默认的、不可删除的唯一索引。
注意
MongoDB有一个特性:超过1024字节的字段不会被加入到索引库中。这意味着即使某个键被设置为唯一索引,只要值超过了8KB,那么即使插入多次也没问题。
去重
如果集合里面对某个键已经有重复的文档,那么对这个键创建唯一索引会失败。如果还是想创建的话,可以暴力一点把他们去重了。注意,这个操作很暴力,别乱用。
1 | db.people.ensureIndex({'username': 1}, {'unique': true, 'dropDups': true}) |
稀疏索引
说实话光看这个名词我真的不知道他想表达啥,这tm谁起的名字!
起因是这样的:当把某个键设置为唯一索引之后,如果插入文档的时候没有这个键,那么对应的值会被设置成null。当下次再次插入这种文档的时候,就会出现null == null
的判断,导致无法插入。
如果想要MongoDB不把空当作null来看带,希望唯一索引支队包含对应键的文档生效,使得当字段存在的时候才需要唯一,那么就使用稀疏索引吧。
使用sparse选项就可以创建稀疏索引。
1 | db.ensureIndex({'email': 1}, {'unique': true, 'sparse': true}) |
A sprase index doesn’t have to be unique.
稀疏索引对查询的结果可能会有影响。影响就是不把不存在当作是null。如果不使用稀疏索引,当使用$ne不等于某个非null值的时候,会包括没这个字段的文档。
强制不使用索引
使用hint()
来表示不使用任何索引。
索引管理
所有的索引存储在MongoDB保留集合system.indexes中,可以执行db.collectionName.getIndexes()
来查看集合上的所有索引信息。
舍弃索引
dropIndex接受两种参数:
- db.test.drop(‘index_name’) 名字字符串
- db.test.drop({‘key’: value}) 索引本身