DBA MongoDB 索引相关

基础知识

索引作用

​ MongoDB的索引与传统的关系型数据库索引的概念基本一致。

​ 除了能够加速查找外,还能针对字段做约束。

​ 在MongoDB中,索引的查找算法也是采用B-Tree算法,具体可参照MySQL系列文章中的索引介绍点我跳转

​ 以下是MongoDB官方所提供的图:

img

索引语法

​ 我们在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 在操作上有一些限制和特点,但是效率更高。

转载声明

​ 关于慢日志这一小节,原文在此,点我跳转

posted @ 2021-03-13 22:03  云崖君  阅读(219)  评论(0编辑  收藏  举报