1. 索引的原理?
索引是对数据库表中一列或多列的值进行排序的一种数据结构,可以让我们查询数据库变得更快。为了方便后续介绍,先科普下MongoDB里的索引机制(同样适用于其他的数据库比如mysql)。
> db.person.find()
{ "_id" : ObjectId("571b5da31b0d530a03b3ce82"), "name" : "jack", "age" : 19 }
{ "_id" : ObjectId("571b5dae1b0d530a03b3ce83"), "name" : "rose", "age" : 20 }
{ "_id" : ObjectId("571b5db81b0d530a03b3ce84"), "name" : "jack", "age" : 18 }
{ "_id" : ObjectId("571b5dc21b0d530a03b3ce85"), "name" : "tony", "age" : 21 }
{ "_id" : ObjectId("571b5dc21b0d530a03b3ce86"), "name" : "adam", "age" : 18 }
当你往某各个集合插入多个文档后,每个文档在经过底层的存储引擎持久化后,会有一个位置信息,通过这个位置信息,就能从存储引擎里读出该文档。为方便介绍,统一用pos(position的缩写)
来代表位置信息。
比如上面的例子里,person
集合里包含插入了5个文档,假设其存储后位置信息如下(为方便描述,文档省去_id字段)
位置信息 | 文档 |
---|---|
pos1 | {“name” : “jack”, “age” : 19 } |
pos2 | {“name” : “rose”, “age” : 20 } |
pos3 | {“name” : “jack”, “age” : 18 } |
pos4 | {“name” : “tony”, “age” : 21} |
pos5 | {“name” : “adam”, “age” : 18} |
假设现在有个查询 db.person.find( {age: 18} )
, 查询所有年龄为18岁的人,这时需要遍历所有的文档(全表扫描),根据位置信息读出文档,对比age字段是否为18。当然如果只有4个文档,全表扫描的开销并不大,但如果集合文档数量到百万、甚至千万上亿的时候,对集合进行全表扫描开销是非常大的,一个查询耗费数十秒甚至几分钟都有可能。
如果想加速 db.person.find( {age: 18} )
,就可以考虑对person表的age字段建立索引。
db.person.createIndex( {age: 1} ) // 按age字段创建升序索引
建立索引后,MongoDB会额外存储一份按age字段升序排序的索引数据,索引结构类似如下,索引通常采用类似btree的结构持久化存储,以保证从索引里快速(O(logN)的时间复杂度
)找出某个age值对应的位置信息,然后根据位置信息就能读取出对应的文档。
AGE | 位置信息 |
---|---|
18 | pos3 |
18 | pos5 |
19 | pos1 |
20 | pos2 |
21 | pos4 |
简单的说,索引就是将文档
按照某个(或某些)字段顺序组织起来,以便能根据该字段高效的查询。
众所周知,MongoDB默认会为插入的文档生成_id字段(如果应用本身没有指定该字段),_id是文档唯一的标识,为了保证能根据文档id快递查询文档,MongoDB默认会为集合创建_id字段的索引。
> db.person.getIndexes() // 查询集合的索引信息
[
{
"ns" : "test.person", // 集合名
"v" : 1, // 索引版本
"key" : { // 索引的字段及排序方向
"_id" : 1 // 根据_id字段升序索引
},
"name" : "_id_" // 索引的名称
}
]
2. MongoDB索引的基本使用
2.1 单字段索引
- 创建单字段索引:
> db.user.ensureIndex({name: 1})
上述语句针对name
创建了单字段索引,其能加速对name
字段的各种查询请求,是最常见的索引形式,MongoDB默认创建的id索引也是这种类型。
{name: 1}
代表升序索引,也可以通过{name: -1}
来指定降序索引,对于单字段索引,升序/降序效果是一样的。
- 查看当前集合的所有索引信息:
> db.user.getIndexes()
- 删除单字段索引:
db.user.dropIndex({name: 1});
2.2 复合索引:
数字1表示name
键的索引按升序存储,-1表示age
键的索引按照降序方式存储。
db.user.ensureIndex({"name":1, "age":-1})
该索引被创建后,基于name
和age
的查询将会用到该索引,或者是基于name
的查询也会用到该索引,但是只基于age
的查询将不会用到该复合索引。因此可以说,如果想用到复合索引,必须在查询条件中包含复合索引中的前N个索引列
。
如果查询条件中的键值顺序和复合索引中的创建顺序不一致的话,MongoDB可以智能的帮助我们调整该顺序,以便使复合索引可以为查询所用。如:
db.user.find({"age": 30, "name": "stephen"})
对于上面示例中的查询条件,MongoDB在检索之前将会动态的调整查询条件的顺序,以使该查询可以用到刚刚创建的复合索引。
对于上面创建的索引,MongoDB都会根据索引的keyname
和索引方向为新创建的索引自动分配一个索引名,下面的命令可以在创建索引时为其指定索引名(userindex
),如:
db.user.ensureIndex({"name":1},{"name":"userindex"})
2.3 唯一索引
在缺省{unique:true}
的情况下创建的索引均不是唯一索引。下面将创建唯一索引,如:
db.user.ensureIndex({"userid":1},{"unique":true})
如果再次插入userid重复的文档时,MongoDB将报错,以提示插入重复键,如:
db.user.insert({"userid":5})
db.user.insert({"userid":5})
E11000 duplicate key error index: user.user.$userid_1 dup key: { : 5.0 }
如果插入的文档中不包含userid键,那么该文档中该键的值为null
,如果多次插入类似的文档,MongoDB将会报出同样的错误:
E11000 duplicate key error index: user.user.$userid_1 dup key: { : null }
3. 使用explain
explain是非常有用的工具,会帮助你获得查询方面诸多有用的信息:
explain会返回查询使用的索引情况,耗时和扫描文档数的统计信息。
3.1 explain( 'executionStats' )
查询具体的执行时间
db.tablename.find().explain( "executionStats" )
输出的如下数值:explain.executionStats.executionTimeMillis
就是当前数据查询所用的时间。
网友评论