DBA MongoDB 索引相关
基础知识
索引作用
MongoDB的索引与传统的关系型数据库索引的概念基本一致。
除了能够加速查找外,还能针对字段做约束。
在MongoDB中,索引的查找算法也是采用B-Tree算法,具体可参照MySQL系列文章中的索引介绍点我跳转。
以下是MongoDB官方所提供的图:
索引语法
我们在MongoDB中建立索引时,需要指定索引字段的排序方式,1为升序,-1为降序,并且索引建立完成后,会生成一个默认的索引名字(当然也可以自己指定)。
以下是创建、查看、删除、修改索引的语法:
# 创建索引
db.集合名.createIndex({索引字段 : 排序})
# 查看所有索引
db.集合名.getIndexes()
# 查看当前集合索引大小
db.集合名.totalIndexSize()
# 删除集合中所有的索引,不包含默认索引
db.集合名.dropIndexes()
# 删除集合中指定的所有
db.集合名.dropIndex("索引名称")
索引排序
在创建索引时,指定排序规则有利于后期的查询。
如,以下表格中是未创建索引时的存储方式:
位置信息 | 文档 |
---|---|
pos1 | |
pos2 | |
pos3 | |
pos4 | |
pos5 |
如果要查询age等于18岁的人,则需要进行全集合扫描。
如果对其做了升序排序,则存储方式就变为了下表中的样子:
age | 位置信息 |
---|---|
18 | pos3 |
18 | pos5 |
19 | pos1 |
20 | pos2 |
21 | pos4 |
索引查看
当创建完一个索引后,可以使用getIndexs()来查看当前集合中所存在的所有索引,如下所示:
> db.collection.drop()
> db.createCollection("collection")
{ "ok" : 1 }
> db.collection.getIndexes()
[
{
"v" : 2, # 索引版本
"key" : { # 索引key
"_id" : 1 # 排序规则
},
"name" : "_id_", # 索引名称
"ns" : "test.collection" # 所属集合
}
]
概念名词
默认索引
MongoDB在集合创建时默认会建立一个基于_id的唯一索引作为文档的主键。
该index无法被删除,名为_id_索引。
聚集索引
聚集索引一个集合只能有一个,包含整个文档的信息(至于包不包含内嵌文档,目前暂时未深入研究)。
MongoDB中的聚集索引就是默认索引_id。
辅助索引
除了聚集索引以外,人为创建的索引都是辅助索引。
查询一个文档时,辅助索引仅包含自身key的value,如果要从辅助索引key中拿取其他key的value,则需要通过回表查询。
其他说明
MongoDB中索引概念应当和MySQL中索引差不多。
我还是推荐你查看之前MySQL索引的文章,对一些概念性的名词有所了解。
索引种类
前言
由于一个key不能创建多个索引,所以下面的某些操作是在删除索引后重新建立的。
不要一味跟着文档敲代码。
单键索引
为某一个键创建单键索引Single Field,如下我们将为name字段建立单键索引:
> db.collection.createIndex({name : 1})
查看索引:
> db.collection.getIndexes()
{
"v" : 2,
"key" : {
"name" : 1
},
"name" : "name_1",
"ns" : "test.collection"
}
复合索引
复合索引Compound Index是指一个索引包含多个字段,使用复合索引时要注意定义字段的顺序与排序规则,这会影响查询效率。
> db.collection.createIndex({"name" : 1, "age" : 1})
查看索引:
> db.collection.getIndexes()
{
"v" : 2,
"key" : {
"name" : 1,
"age" : 1
},
"name" : "name_1_age_1",
"ns" : "test.collection"
}
多键索引
多键索引Multikey Index是指为内嵌的文档、或者数组建立索引,如下所示,建立多key索引后可以方便查询具有相同爱好的文档:
> db.collection.createIndex({"hobby" : 1})
查看索引:
> db.collection.getIndexes()
{
"v" : 2,
"key" : {
"hobby" : 1
},
"name" : "hobby_1",
"ns" : "test.collection"
}
同理,我们也可以为内嵌文档来建立内嵌索引,如下所示,建立多key索引后方便以Js成绩为筛选条件来查看对应的文档:
> db.collection.createIndex({"grades.Js" : 1})
查看索引:
> db.collection.getIndexes()
{
"v" : 2,
"key" : {
"grades.Js" : 1
},
"name" : "grades.Js_1",
"ns" : "test.collection"
}
哈希索引
哈希索引Hashed Indexes可以将字段的值进行哈希计算后作为索引,常用于定值查找,而不适用于范围查找。
如将手机号作为哈希索引来说是非常明智的一个选择,需要注意的是使用哈希索引不需要进行排序:
> db.collection.createIndex({"phone" : "hashed"})
查看索引:
> db.collection.getIndexes()
{
"v" : 2,
"key" : {
"phone" : "hashed"
},
"name" : "phone_hashed",
"ns" : "test.collection"
}
文本索引
MongoDB文本索引可以将一篇文章中的任意词汇,句子用作查询条件。
但是一般来讲这种全局搜索类的业务我们不会用MongoDB来做,而是使用另一款NoSQL产品Elasticsearch。
所以文本索引仅做了解即可,可以参阅官方文档:点我跳转
地理空间索引
MongoDB地理空间索引一般用于地图类应用的开发中,所以相对来说使用场景也较少。
更多的Web开发者会选择第三方API进行调用,而不是自己实现,因此对于普通开发人员仅做了解即可。
可以参阅官方文档:点我跳转
其他条件
background
在创建索引时,默认会锁集合(锁表),加上该属性则不会锁集合,但是会降低建立索引的速度。
> db.collection.createIndex({"name" : 1}, {"background" : true})
name
在建立索引时,如果没有手动指定索引名字,则会自动生成一个名字,使用该属性可手动指定索引名:
> db.collection.createIndex({"name" : 1}, {"name" : "index_name"})
Unique Indexs
为索引建立唯一性约束:
> db.collection.createIndex({"name" : 1}, {"unique" : true})
也可以进行复合索引的唯一约束:
> db.collection.createIndex({"name" : 1 , "age" : 1}, {"unique" : true})
Sparse Indexs
稀疏索引的条件,举个例子,有的文档有addr字段,而有的文档没有addr字段,这个时候就可以用上稀疏索引。
如果为addr创建普通索引的话,即便没有addr字段的文档也会对其设置一个{“addr" : null}的字段。
而稀疏索引就不会这么做,只针对有addr字段的文档建立索引,而没有addr字段的文档则不建立索引,更不会去添加字段。
> db.collection.createIndex({"addr" : 1}, {"sparse" : 1})
TTL Indexes
过期索引的条件,若一个文档在一段时间后要求删除,则可以添加一个时间字段。
当时间到达后,该文档则会被MongoDB删除。
> db.items.insertMany({"createTime" : new Date()})
> db.items.createIndex({"createTime" : 1, {expireAfterSeconds : 120}})
# 以秒为单位,120s后对该文档进行删除
# 过期时间 = 字段时间 + expireAfterSeconds设定时间
正确使用
最左前缀匹配
仅适用于复合索引上。
如索引建立规则是{“name” : 1, “age” : 1, “addr” : 1}。
则必须遵循最左前缀匹配规则,如下匹配都是能成功走索引的:
> db.collection.find({"name" : "Jack", "age" : 18, "addr" : "Bj"})
如果没有遵循顺序,如下这种查询是不走索引的:
> db.collection.find({"name" : "Jack", "addr" : "Bj"})
> db.collection.find({"age" : 18, "addr" : "Bj"})
注意,MongoDB与MySQL不同,MySQL中如果用了多个等值查询,则不用遵循最左前缀匹配的规则
而对于MongoDB来说,即使是多个等值查询,也依然不会走索引
筛选与排序
有的时候,查询条件的筛选字段是做了索引的,如name这个筛选字段我们给他做了索引。
但是进行sort()排序时,排序字段是createTime字段,这个时候还是会降低运行效率。
正确的做法是,为createTime字段也加上索引。
低性能修改器
$where与$exists以及null都是性能极低的修改器,完全不能使用索引。
$ne和$not以及$nor还有$nin等,虽然能使用索引,但是会进行全索引扫描,性能也很低。
其他的建议
参照MySQL中索引即可,如覆盖索引,回表查询,索引合并(MongoDB中称为交叉索引),索引递推等。
点我跳转
执行计划
执行计划
explain()是一个语句调优中经常使用到的函数,它可以对增伤改查等任意对文档的语句做出性能评估,拿到优化器选择后的最优策略进行分析(并不会执行语句本身)。
使用explain()函数来查看语句的执行计划,下面是没使用了普通索引的一次扫描:
> db.collection.find({"name" : "Jack"}).explain()
{
# 缺省模式,
"queryPlanner" : {
"plannerVersion" : 1, # 查询计划版本
"namespace" : "test.collection", # 被查询对象
"indexFilterSet" : false, # 是否使用到了索引来过滤
"parsedQuery" : { # 查询条件
"name" : {
"$eq" : "Jack"
}
},
"queryHash" : "01AEE5EC",
"planCacheKey" : "4C5AEA2C",
"winningPlan" : { # 最佳的执行计划
"stage" : "FETCH", # 第一阶段:FETCH根据索引检索文档(COLLSCAN是全集合扫描文档)
"inputStage" : {
"stage" : "IXSCAN", # 第二阶段:IXSCAN索引扫描
"keyPattern" : {
"name" : 1
},
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"Jack\", \"Jack\"]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "centos",
"port" : 27017,
"version" : "4.2.12",
"gitVersion" : "5593fd8e33b60c75802edab304e23998fa0ce8a5"
},
"ok" : 1
}
由于篇幅原因,这里不再做过多赘述,我找到一篇很不错的文章,如果你对执行计划感兴趣,可以点我跳转
慢日志
开启Profiling功能
有两种方式可以控制 Profiling 的开关和级别,第一种是直接在启动参数里直接进行设置。
启动MongoDB时加上--profile=级别即可。
也可以在客户端调用db.setProfilingLevel(级别) 命令来实时配置。可以通过db.getProfilingLevel()命令来获取当前的Profile级别。
> db.setProfilingLevel(2);
{"was" : 0 , "ok" : 1}
> db.getProfilingLevel()
上面斜体的级别可以取0,1,2 三个值,他们表示的意义如下:
0 – 不开启
1 – 记录慢命令 (默认为>100ms)
2 – 记录所有命令
Profile记录在级别1时会记录慢命令,那么这个慢的定义是什么?上面我们说到其默认为100ms,当然有默认就有设置,其设置方法和级别一样有两种,一种是通过添加–slowms启动参数配置。第二种是调用db.setProfilingLevel时加上第二个参数:
db.setProfilingLevel( level , slowms )
db.setProfilingLevel( 1 , 10 );
查询Profiling记录
与MySQL的慢查询日志不同,Mongo Profile 记录是直接存在系统db里的,记录位置 system.profile,所以,我们只要查询这个Collection的记录就可以获取到我们的 Profile 记录了。
> db.system.profile.find()
{"ts" : "Thu Jan 29 2009 15:19:32 GMT-0500 (EST)" , "info" : "query test.$cmd ntoreturn:1 reslen:66 nscanned:0 query: { profile: 2 } nreturned:1 bytes:50" , "millis" : 0}
db.system.profile.find( { info: /test.foo/ } )
{"ts" : "Thu Jan 29 2009 15:19:40 GMT-0500 (EST)" , "info" : "insert test.foo" , "millis" : 0}
{"ts" : "Thu Jan 29 2009 15:19:42 GMT-0500 (EST)" , "info" : "insert test.foo" , "millis" : 0}
{"ts" : "Thu Jan 29 2009 15:19:45 GMT-0500 (EST)" , "info" : "query test.foo ntoreturn:0 reslen:102 nscanned:2 query: {} nreturned:2 bytes:86" , "millis" : 0}
{"ts" : "Thu Jan 29 2009 15:21:17 GMT-0500 (EST)" , "info" : "query test.foo ntoreturn:0 reslen:36 nscanned:2 query: { $not: { x: 2 } } nreturned:0 bytes:20" , "millis" : 0}
{"ts" : "Thu Jan 29 2009 15:21:27 GMT-0500 (EST)" , "info" : "query test.foo ntoreturn:0 exception bytes:53" , "millis" : 88}
列出执行时间长于某一限度(5ms)的 Profile 记录:
> db.system.profile.find( { millis : { $gt : 5 } } )
{"ts" : "Thu Jan 29 2009 15:21:27 GMT-0500 (EST)" , "info" : "query test.foo ntoreturn:0 exception bytes:53" , "millis" : 88}
查看最新的 Profile 记录:
> db.system.profile.find().sort({$natural:-1})
Mongo Shell 还提供了一个比较简洁的命令show profile,可列出最近5条执行时间超过1ms的 Profile 记录。
Profile信息内容详解
- ts-该命令在何时执行。
- millis Time-该命令执行耗时,以毫秒记.
- info-本命令的详细信息.
- query-表明这是一个query查询操作.
- ntoreturn-本次查询客户端要求返回的记录数.比如, findOne()命令执行时 ntoreturn 为 1.有limit(n) 条件时ntoreturn为n.
- query-具体的查询条件(如x>3).
- nscanned-本次查询扫描的记录数.
- reslen-返回结果集的大小.
- nreturned-本次查询实际返回的结果集.
- update-表明这是一个update更新操作.
- fastmod-Indicates a fast modify operation. See Updates. These operations are normally quite fast.
- fastmodinsert – indicates a fast modify operation that performed an upsert.
- upsert-表明update的upsert参数为true.此参数的功能是如果update的记录不存在,则用update的条件insert一条记录.
- moved-表明本次update是否移动了硬盘上的数据,如果新记录比原记录短,通常不会移动当前记录,如果新记录比原记录长,那么可能会移动记录到其它位置,这时候会导致相关索引的更新.磁盘操作更多,加上索引更新,会使得这样的操作比较慢.
- insert-这是一个insert插入操作.
- getmore-这是一个getmore 操作,getmore通常发生在结果集比较大的查询时,第一个query返回了部分结果,后续的结果是通过getmore来获取的。
MongoDB查询优化
如果nscanned(扫描的记录数)远大于nreturned(返回结果的记录数)的话,那么我们就要考虑通过加索引来优化记录定位了。
reslen 如果过大,那么说明我们返回的结果集太大了,这时请查看find函数的第二个参数是否只写上了你需要的属性名。(类似 于MySQL中不要总是select *)
对于创建索引的建议是:如果很少读,那么尽量不要添加索引,因为索引越多,写操作会越慢。如果读量很大,那么创建索引还是比较划算的。(和RDBMS一样,貌似是废话 -_-!!)
MongoDB更新优化
如果写查询量或者update量过大的话,多加索引是会有好处的。以及~~~~(省略N字,和RDBMS差不多的道理)
Use fast modify operations when possible (and usually with these, an index). See Updates.
Profiler的效率
Profiling 功能肯定是会影响效率的,但是不太严重,原因是他使用的是 system.profile 来记录,而 system.profile 是一个capped collection 这种collection 在操作上有一些限制和特点,但是效率更高。
转载声明
关于慢日志这一小节,原文在此,点我跳转