1-MongoDB - 索引

about

MongoDB3.6.12 + Centos7.9

索引相关的MongoDB官档:https://docs.mongodb.com/v3.6/indexes/

索引的主要作用就是提高查询效率。

MongoDB中的索引有以下特点:

  • MongoDB索引使用B-Tree作为数据结构。
  • MongoDB的索引也是一个集合,但它要比原文档集合小得多。
  • MongoDB使用索引中的排序返回结果。

在后续的示例中用到了执行计划,执行计划指的是MongoDB将会如何执行我们的相关语句,关于执行计划,点我

索引类型

MongoDB中支持以下几种索引类型:

  • 单列索引(Single Field)。
  • 复合索引(Compound Indexes)。
  • 多键索引(Multikey Indexes)。
  • 文本索引(Text Indexes)。
  • 哈希索引(Hashed Indexes)。

在了解每种索引的细节之间,先来简单的了解索引的基本管理。

索引管理

索引管理部分主要了解如何创建、查看、删除索引。

你可能会有疑问,为啥没有修改索引?答案是除了TTL索引之外,修改索引就是删除并重新创建索引。

创建索引

在MongoDB3.0.0版本之间,使用ensureIndex方法来创建索引,而在之后的版本中,虽然ensureIndex方法还能使用,但推荐使用createIndex方法来代替,其语法参考如下:

db.collection.createIndex({"key": 1},options)

参数解释:

Parameter Type Description
keys document key:value格式的文档,其中key表示索引键,value描述了索引类型,1表示升序,-1表示降序。
options document 可选参数,包含一组控制索引创建的选项文档,有关详细信息请参见options详情表。

options参数列表:

Parameter Type Description
background Boolean 建索引过程会阻塞其他数据库操作,background可指定以后台方式创建索引,默认值为false。
unique Boolean 建立的索引是否唯一,指定为true创建唯一索引,默认为false。
name String 索引的名称,如果未指定,MongoDB通过连接索引的字段名和排序顺序生成默认的索引名。
droupDups Boolean 3.x版本已废弃。在建立唯一索引时是否删除重复记录,指定true创建唯一索引,默认值为false。
sparse Boolean 对文档中不存在的字段数据不启用索引,这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档,默认值为false。
expireAfterSeconds Integer 指定一个以秒为单位的数值,完成TTL设定,设定集合的生存时间。
v Index Version 索引引擎的版本,默认的版本取决于创建索引时引擎的版本号。
weights Document 数值在1~99999之间的权重值,表示该索引相对于其他索引字段的得分权重。
default_language String 对于文本索引,该参数决定了停用词及词干分词器的规则列表,默认为英语。
language_override String 对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值language。

示例:

// 创建单列索引
db.s7.createIndex({"myIndex1": 1})
{
	"createdCollectionAutomatically" : false,
	"numIndexesBefore" : 1,
	"numIndexesAfter" : 2,
	"ok" : 1
}

// 创建复合索引
db.collection.createIndex({"user_id": 1, "score": 1})

// 创建多键索引
db.collection.createIndex({"info.address": 1})

查看索引

// 列出当前数据库中,所有索引
db.getCollectionNames().forEach(function(collection) {
   indexes = db[collection].getIndexes();
   print("Indexes for " + collection + ":");
   printjson(indexes);
});

// 查询指定集合中所有索引信息
db.s7.getIndices()
db.s7.getIndexes()
[
	{	// 一个字典存储一个索引的基本信息
		"v" : 2,	// 索引引擎的版本号
		"key" : {	// 索引字段
			"_id" : 1  // 1:升序排序
		},
		"name" : "_id_",  // 索引名称
		"ns" : "t1.s7"		// 索引所在的 namespace 
	},
	{
		"v" : 2,
		"key" : {
			"myIndex1" : 1
		},
		"name" : "myIndex1_1",  // MongoDB拼接后的自定义索引
		"ns" : "t1.s7"
	}
]

// 返回集合中所有索引的keys
db.s7.getIndexKeys()
[ { "_id" : 1 }, { "myIndex1" : 1 } ]

// 返回集合中索引空间
db.s7.getIndexSpecs() // 等价于 db.s7.getIndexes()

删除索引

删除索引有以下几种方式:

// 根据特定索引
db.s7.dropIndex({"name": 1})

// 根据索引名称删除
db.s7.dropIndex("name_index")  // name_index:索引名称

// 清除集合中所有索引,如果存在_id这个内置索引,则该索引不会被清除
db.s7.dropIndexes()

索引类型

MongoDB中支持以下几种索引类型:

  • 单列索引(Single Field)。
  • 复合索引(Compound Indexes)。
  • 多键索引(Multikey Indexes)。
  • 文本索引(Text Indexes)。
  • 哈希索引(Hashed Indexes)。

常用索引介绍

默认索引

默认的,在创建集合期间,MongoDB会在_id字段上创建唯一索引,用来防止客户端插入两个具有相同值的文档,我们也不能删除该默认索引,而通常我们在插入文档时,应该忽略该字段,让ObjectId对象来自动生成。

// 准备一个新的集合并插入数据
db.s1.drop()
db.s1.insertMany([
    {"name": "zhangkai", "age": 18},
    {"name": "likai", "age": 20}
])
{
	"acknowledged" : true,
	"insertedIds" : [	// 自动生成的两个文档的 _id
		ObjectId("600fe7e79ab2f8c54a73ea77"),  
		ObjectId("600fe7e79ab2f8c54a73ea78")
	]
}

db.s1.find()
{ "_id" : ObjectId("600fe7e79ab2f8c54a73ea77"), "name" : "zhangkai", "age" : 18 }
{ "_id" : ObjectId("600fe7e79ab2f8c54a73ea78"), "name" : "likai", "age" : 20 }

// 可以根据 _id 进行过滤
db.s1.find({"_id": ObjectId("600fe7e79ab2f8c54a73ea78")})
{ "_id" : ObjectId("600fe7e79ab2f8c54a73ea78"), "name" : "likai", "age" : 20 }

单列索引

Single Field Indexes

MongoDB支持在文档的单个字段上创建自定义的升序/降序索引,称为——单列索引(Single Field Index),也可以称之为单字段索引。

在单列索引中,升序/降序并不影响查询性能。

创建单列索引

// 为 age 字段创建索引
db.s1.createIndex({"age": 1})

// 如下的查询将会走索引
db.s1.find({"age": {"$gt": 10}})
{ "_id" : ObjectId("600fe7e79ab2f8c54a73ea77"), "name" : "zhangkai", "age" : 18 }
{ "_id" : ObjectId("600fe7e79ab2f8c54a73ea78"), "name" : "likai", "age" : 20 }

// 从执行计划中,查看是否走了索引
db.s1.find({"age": {"$gt": 10}}).explain()["queryPlanner"]["winningPlan"]
{
	"stage" : "FETCH",
	"inputStage" : {
		"stage" : "IXSCAN",  // 走了索引扫描
		"keyPattern" : {
			"age" : 1
		},
		"indexName" : "age_1",  // 使用的索引
		"isMultiKey" : false, 
		"multiKeyPaths" : {
			"age" : [ ]
		},
		"isUnique" : false,
		"isSparse" : false,
		"isPartial" : false,
		"indexVersion" : 2,
		"direction" : "forward",
		"indexBounds" : {
			"age" : [
				"(10.0, inf.0]"
			]
		}
	}
}

在嵌入式字段上创建单列索引

// 准备一个新的集合并插入数据
db.s1.drop()
db.s1.insertMany([
    {"name": "zhangkai", "age": 18, "info": {"address": "beijing", "tel": "13011304424"}},
    {"name": "likai", "age": 20, "info": {"address": "shanghai", "tel": "15011304424"}}
])

// 创建索引
db.s1.createIndex({"info.address": 1})

// 查询
db.s1.find({"info.address": "beijing"})
{ "_id" : ObjectId("600fec019ab2f8c54a73ea79"), "name" : "zhangkai", "age" : 18, "info" : [ { "address" : "beijing" }, { "tel" : "13011304424" } ] }
db.s1.find({"info.address": "beijing"}).explain()["queryPlanner"]["winningPlan"]
{
	"stage" : "FETCH",
	"inputStage" : {
		"stage" : "IXSCAN",  // 走了索引查询
		"keyPattern" : {
			"info.address" : 1
		},
		"indexName" : "info.address_1",  // 使用的索引
		"isMultiKey" : false,
		"multiKeyPaths" : {
			"info.address" : [ ]
		},
		"isUnique" : false,
		"isSparse" : false,
		"isPartial" : false,
		"indexVersion" : 2,
		"direction" : "forward",
		"indexBounds" : {
			"info.address" : [
				"[\"beijing\", \"beijing\"]"
			]
		}
	}
}

在嵌入式文档上创建单列索引

// 创建索引
db.s1.createIndex({"info": 1})

// 查询
// 有结果返回
db.s1.find({"info": {"address": "shanghai", "tel": "15011304424"}})
// db.s1.find({"info": {"tel": "15011304424","address": "shanghai"}})

注意,当对嵌入式文档执行相等匹配时,字段顺序很重要,嵌入式文档必须完全匹配,才能返回结果。

另外,在嵌入式文档和嵌入式字段创建的索引不能混为一谈:

  • 在嵌入式文档上创建的索引,会对整个嵌入的文档进行索引,它是一个整体,查询时,要进行完全匹配。
  • 在嵌入式字段上创建的索引,只是对嵌入文档的指定字段进行索引,索引部分只包含嵌入文档的指定字段。

复合索引

Compound Indexes

MongoDB还支持多字段自定义索引,即复合索引(Compound Indexes),也可以称之为组合索引、联合索引。MongoDB中的复合索引在某些方面跟关系型数据库的组合索引是一样的,比如同样支持索引前缀。

创建复合索引

// 准备一个新的集合并插入数据
db.s2.drop()
db.s2.insertMany([
    {"userid": 1, "name": "zhangkai", "age": 18, "score": 98, "info": {"address": "beijing", "tel": "13011304424"}},
    {"userid": 2, "name": "likai", "age": 20,  "score": 88, "info": {"address": "shanghai", "tel": "15011304424"}},
    {"userid": 3, "name": "wangkai", "age": 20,  "score": 90, "info": {"address": "nanjing", "tel": "15111304442"}},
])

// 创建复合索引,首先根据 userid 字段升序排序,当 userid 字段相同时,再根据 score 字段降序排序
db.s2.createIndex({"userid": 1, "score": -1}, {"name": "compoundIndex1"})

复合索引与排序

复合索引中字段的顺序非常重要,例如下图中的复合索引由{userid:1, score:-1}组成,则该复合索引首先按照userid升序排序;然后再每个userid的值内,再按照score降序排序。

在复合索引中,按照何种方式排序,决定了该索引在查询中是否能被应用到。

来看一下之前创建的复合索引compoundIndex1在排序中的应用情况:

// 如下排序走复合索引 compoundIndex1
db.s2.find().sort({"userid": 1, "score": -1})
db.s2.find().sort({"userid": -1, "score": 1})

// 如下排序不走复合索引 compoundIndex1
db.s2.find().sort({"userid": 1, "score": 1})
db.s2.find().sort({"userid": -1, "score": -1})
db.s2.find().sort({"score": 1, "userid": -1})
db.s2.find().sort({"score": 1, "userid": 1})
db.s2.find().sort({"score": -1, "userid": -1})
db.s2.find().sort({"score": -1, "userid": 1})

// 上述情况可以通过 explain 进行查看,如:
db.s2.find().sort({"score": -1, "userid": 1}).explain()

复合索引与索引前缀

复合索引同样支持对索引前缀的查询,例如,考虑以下复合索引:

// 三个字段的复合索引
{"userid": 1, "socore": 1, "age": 1}

// 上面的复合索引有以下索引前缀
{"userid": 1}
{"userid": 1, "score": 1}

在以下情况的查询走索引:

  • userid
  • userid + score
  • userid + score + age
  • userid + age,尽管索引被使用,但效率不高。
// 为了避免混淆,先清空索引
db.s2.dropIndexes()
// 创建索引
db.s2.createIndex({"userid": 1, "socore": 1, "age": 1}, {"name": "compoundIndex2"})
    
// userid  走索引
db.s2.find({"userid": {"$lt": 3}}).explain()

// userid + score  走索引
db.s2.find({"userid": {"$lt": 3}, "score": {"$lt": 98}}).explain()

// userid + score + age   走索引
db.s2.find({"userid": {"$lt": 3}, "score": {"$lt": 98}, "age": {"$lt": 30}}).explain()

// userid + age   走索引
db.s2.find({"userid": {"$lt": 3}, "age": {"$lt": 30}}).explain()

以下情况不走索引:

  • score
  • age
  • score + age
// score  不走索引
db.s2.find({"score": {"$lt": 98}}).explain()

// age  不走索引
db.s2.find({"age": {"$lt": 30}}).explain()

// score + age   不走索引
db.s2.find({"score": {"$lt": 98}, "age": {"$lt": 30}}).explain()

多键索引

Multikey Indexes

对于包含数组的文档,我们可以使用MongoDB提供了多键索引,为数组中的每个元素创建一个索引键,这些多键索引支持对数组字段的有效查询。

创建多键索引

// 准备集合并插入数据
db.s3.drop()
db.s3.insertMany([
    { _id: 5, type: "food", item: "aaa", ratings: [ 5, 8, 9 ]},
    { _id: 6, type: "food", item: "bbb", ratings: [ 5, 9 ]},
    { _id: 7, type: "food", item: "ccc", ratings: [ 9, 5, 8 ]},
    { _id: 8, type: "food", item: "ddd", ratings: [ 9, 5 ] },
    { _id: 9, type: "food", item: "eee", ratings: [ 5, 9, 5 ]}
])

// 基于ratings字段创建多键索引
db.s3.createIndex({ratings:1})

基于一个数组创建索引,MongoDB会自动创建为多键索引,无需刻意指定,另外,多键索引不等于复合索引。

来看下刚才创建的多键索引的应用情况:

// 查询ratings数组中等于 5和9 的文档,所以,只有 _id:6 的文档符合条件
db.s3.find({ratings:[5, 9]})

// 来看执行计划
db.s3.find({ratings:[5, 9]}).explain()["queryPlanner"]["winningPlan"]
{
	"stage" : "FETCH",
	"filter" : {
		"ratings" : {
			"$eq" : [
				5,
				9
			]
		}
	},
	"inputStage" : {
		"stage" : "IXSCAN",  
		"keyPattern" : {
			"ratings" : 1
		},
		"indexName" : "ratings_1",
		"isMultiKey" : true,   // 走了多键索引
		"multiKeyPaths" : {
			"ratings" : [
				"ratings"
			]
		},
		"isUnique" : false,
		"isSparse" : false,
		"isPartial" : false,
		"indexVersion" : 2,
		"direction" : "forward",
		"indexBounds" : {
			"ratings" : [
				"[5.0, 5.0]",
				"[[ 5.0, 9.0 ], [ 5.0, 9.0 ]]"
			]
		}
	}
}

你可能对上面的查询结果有点疑惑,按道理说,只要ratings数组中包含5和9的文档,都应该被返回,但很遗憾不能这么理解,因为db.s3.find({ratings:[5, 9]})条件这么写,相当于[5, 9]是一个整体,文档的ratings数组的元素必须完全等于5和9才被返回。

而使用下面这种查询,才能返回你想要的结果:

// 只要数组包含5的文档都会被返回,并且走多键索引
db.s3.find({ratings:5})

嵌套文档中的多键索引的应用

// 准备集合并插入数据
db.s3.drop()
db.s3.insertMany([
    {
      _id: 1,
      item: "abc",
      stock: [
        { size: "S", color: "red", quantity: 25 },
        { size: "S", color: "blue", quantity: 10 },
        { size: "M", color: "blue", quantity: 50 }
      ]
    },
    {
      _id: 2,
      item: "def",   
      stock: [       
        { size: "S", color: "blue", quantity: 20 },
        { size: "M", color: "blue", quantity: 5 },
        { size: "M", color: "black", quantity: 10 },
        { size: "L", color: "red", quantity: 2 }
      ]
    },
    {
      _id: 3,
      item: "ijk",
      stock: [
        { size: "M", color: "blue", quantity: 15 },
        { size: "L", color: "blue", quantity: 100 },
        { size: "L", color: "red", quantity: 25 }
      ]
    }
])

// 基于嵌套文档创建多键索引
db.s3.createIndex({"stock.size":1, "stock.quantity":1})

来看下刚才创建的多键索引的应用情况,下面几种情况都会走多键索引:

// 查询嵌套文档 stock.size为 S 的 执行计划
db.s3.find({"stock.size": "S"}).explain()["queryPlanner"]["winningPlan"]

// 条件过滤
db.s3.find({"stock.size": "S", "stock.quantity": {"$gt":20}}).explain()["queryPlanner"]["winningPlan"]

// 排序
db.s3.find().sort({"stock.size": 1, "stock.quantity": 1}).explain()["queryPlanner"]["winningPlan"]

// 过滤加排序
db.s3.find({"stock.size": "S"}).sort({"stock.quantity": 1}).explain()["queryPlanner"]["winningPlan"]

其他索引

MongoDB还支持地理空间索引(Geospatial Indexes)、文本索引(Text Indexes)、哈希索引(Hashed Indexes)。

地理空间索引(Geospatial Indexes)

为了支持对于地理空间坐标数据的有效查询,MongoDB提供了两种特殊的索引:

  • 返回结果时使用平面几何的二维索引。
  • 返回结果时使用球面几何的二维索引。

文本索引(Text Indexes)

MongoDB提供了一种文本索引类型,支持在集合中搜索字符串内容。

这些文本索引不存储特定语言的停用词(例如theaor),而是将集合中的词作为词干,只存储词根。

哈希索引(Hashed Indexes)

为了支持基于散列的分片,MongoDB提供了散列索引类型,它对字段值的散列进行索引,这些索引在其范围内的值分布更加随机,但支持相等匹配,不支持基于范围的查询。

索引属性

除了 MongoDB支持的众多索引类型外,索引还可以具有各种属性。一起来看看在创建索引时可以选择的索引属性。

TTL索引

什么是TTL索引

TTL(Time To Live)索引是特殊的单列索引,通过在创建索引时指定expireAfterSeconds参数将普通的单列索引标记为TTL索引,实现为文档的自动过期删除功能。

TTL索引运行原理

  • MongoDB会开启一个后台线程读取该TTL索引的值来判断文档是否过期,但不会保证已过期的数据会立马被删除,因后台线程每60秒触发一次删除任务,且如果删除的数据量较大,会存在上一次的删除未完成,而下一次的任务已经开启的情况,导致过期的数据也会出现超过了数据保留时间60秒以上的现象。
  • 对于副本集而言,TTL索引的后台进程只会在primary节点开启,在从节点会始终处于空闲状态,从节点的数据删除是由主库删除后产生的oplog来做同步。
  • TTL索引除了有expireAfterSeconds属性外,和普通索引一样

TTL索引的使用

第一种应用场景:为所有插入的文档指定一个统一的过期时间。

指定具体的过期时间,后续插入的记录都会在expireAfterSeconds指定的时间(单位:秒)后自动删除:

// 创建 TTL索引 并指定过期时间为 30 秒
db.s9.createIndex({"logTTL": 1}, {"expireAfterSeconds": 30})  // 指定过期时间是 30 秒
db.s9.insertMany([
    {"logMessage": "error", "logTTL": new Date()}, // logTTL类型是Date,会自动过期
    {"logMessage": "error", "logTTL": "2020-12-12"},  // logTTL类型是string,不会自动过期
])

// 在30秒后,查询,只剩下不会自动过期的那条记录了
db.s9.find()
{ "_id" : ObjectId("600f7fe064bc3da87653e9db"), "logMessage" : "error", "logTTL" : "2020-12-12" }

第二种应用场景:插入文档时时,自己指定过期时间。

就是创建索引期间,将expireAfterSeconds的值设置为0,让每篇文档自己指定何时过期:

db.s9.dropIndex({"logTTL": 1})
db.s9.createIndex({"logTTL": 1}, {"expireAfterSeconds": 0})

// 文档的过期时间,就是 logTTL 字段指定的时间
db.s9.insertMany([
    {"logMessage": "error", "logTTL": new Date("January 26, 2021 10:56:00")},
    {"logMessage": "error", "logTTL": [
        new Date("January 26, 2021 10:56:00"),
        new Date("January 27, 2021 10:56:00"),
        new Date("January 28, 2021 10:56:00")
    ]}  // 如果数组类型,且其中有个多个日期类型的值,将以最早的时间作为过期时间
])

那么如何修改expireAfterSeconds的值呢?除了删除重建索引之外,也可以通过collMod`来进行设置:

// 之前的TTL索引 expireAfterSeconds 值为0
db.s9.getIndexes()[1]["expireAfterSeconds"]
0

// 现在修改为60
// 在 s9 集合所在的库中
db.runCommand(
    {
        "collMod": "s9",  // 指定集合名称
        "index": {
            "keyPattern": {"logTTL": 1},  // 指定索引
            "expireAfterSeconds": 60  // 新的过期时间
        }
    }
)
// 返回结果
{ "expireAfterSeconds_old" : 0, "expireAfterSeconds_new" : 60, "ok" : 1 }

TTL索引的使用限制

  • TTL索引只支持单例索引,复合索引不支持TTL。
  • _id字段不支持TTL索引。
  • 无法在上限集合上创建TTL索引,因为MongoDB无法从上限集合中删除文档。
  • 如果某个字段已经存在非TTL索引,那么在该字段上无法再创建TTL索引。

关于上限集合,点我

唯一索引

唯一索引(Unique Indexes)可确保索引字段不会存储重复值;即对索引字段实施唯一性。默认情况下,MongoDB 在创建集合时会在_id字段上创建唯一索引。

创建唯一索引

// 创建单列唯一索引
// unipue:true声明普通单列索引为唯一索引
db.userinfo.createIndex({"user": 1}, {"unique": true})


// 复合索引中的添加唯一属性
db.userinfo.createIndex({"user": 1, "tel": 1}, {"unique": true})

// 多键索引中添加唯一属性
db.userinfo.createIndex({"info.address": 1, "info.tel": 1}, {"unique": true})

唯一索引的一些限制

对于那些已经存在的非唯一的列,在其上面创建唯一索引将失败:

// 数据是这样的
db.s10.insertMany([
    {"name": "zhangkai"},
    {"name": "zhangkai"}
])
// 创建唯一索引会报错
db.s10.createIndex({"name": 1}, {"unique": true})
{
	"ok" : 0,
	"errmsg" : "E11000 duplicate key error collection: t1.s10 index: name_1 dup key: { : \"zhangkai\" }",
	"code" : 11000,
	"codeName" : "DuplicateKey"  // 重复键
}

对于数组类型的key,相同的值只能插入一次:

// 清空 s10
db.s10.remove({})

// 插入数据
db.s10.insert({"info": [{"tel": 13011303330}]})

// 创建唯一索引
db.s10.createIndex({"info.tel": 1}, {"unique": true})

// 再次插入相同的值,就报错了
db.s10.insert({"info": [{"tel": 13011303330}]})
WriteResult({
	"nInserted" : 0,
	"writeError" : {
		"code" : 11000,
		"errmsg" : "E11000 duplicate key error collection: t1.s10 index: info.tel_1 dup key: { : 13011303330.0 }"
	}
})

MongoDB只允许一篇文档缺少索引字段:

// 清空 s10
db.s10.remove({})

// 插入数据,成功
db.s10.insert({"name": "zhangkai"})

// 创建唯一索引,成功
db.s10.createIndex({"name": 1}, {"unique": true})

// 插入重复则报错,符合预期
db.s10.insert({"name": "zhangkai"})  // "errmsg" : "E11000 duplicate key error collection: t1.s10 index: name_1 dup key: { : \"zhangkai\" }"

// 插入一个缺少 name 字段的文档,可以成功
db.s10.insert({"age": 18})  // mongodb会默认为 name 字段设置为null

// 再次插入缺少 name 字段的文档,就会失败,因为mongodb只允许一篇文档缺少索引字段
db.s10.insert({"age": 20})  // "errmsg" : "E11000 duplicate key error collection: t1.s10 index: name_1 dup key: { : null }"         

另外,不能对哈希索引指定唯一约束。

稀疏索引

稀疏索引(Sparse Indexes)也叫做间隙索引,它只包含含有索引字段的文档,如果某个文档的不存在索引键,则跳过,所以,这种索引被称之为稀疏索引。

创建稀疏索引

// 准备数据
db.s11.insertMany([
    {"name": "zhangkai"},
    {"name": "likai", "score": 95},
    {"name": "wangkai", "score": 92},
])

// 在创建索引时,指定 sparse:true 将普通索引标记为稀疏索引
db.s11.createIndex({"score": 1}, {"sparse": true})

// 通过查询语句的执行计划,查看稀疏索引的应用情况
db.s11.find({"score": {"$lt": 95}})
{ "_id" : ObjectId("600fb8d164bc3da87653e9f4"), "name" : "wangkai", "score" : 92 }
db.s11.find({"score": {"$lt": 95}}).explain()["queryPlanner"]["winningPlan"]
{
	"stage" : "FETCH",  // 根据索引检索指定的文档
	"inputStage" : {
		"stage" : "IXSCAN",  // 使用了索引扫描
		"keyPattern" : {
			"score" : 1
		},
		"indexName" : "score_1",  // 索引名称
		"isMultiKey" : false,
		"multiKeyPaths" : {
			"score" : [ ]
		},
		"isUnique" : false,
		"isSparse" : true,  // 索引类型是稀疏索引
		"isPartial" : false,
		"indexVersion" : 2,
		"direction" : "forward",
		"indexBounds" : {
			"score" : [
				"[-inf.0, 95.0)"
			]
		}
	}
}

再来看稀疏索引无法使用的示例:

db.s11.find().sort({"score": 1})
{ "_id" : ObjectId("600fb8d164bc3da87653e9f2"), "name" : "zhangkai" }
{ "_id" : ObjectId("600fb8d164bc3da87653e9f4"), "name" : "wangkai", "score" : 92 }
{ "_id" : ObjectId("600fb8d164bc3da87653e9f3"), "name" : "likai", "score" : 95 }

db.s11.find().sort({"score": 1}).explain()["queryPlanner"]["winningPlan"]
{
	"stage" : "SORT",
	"sortPattern" : {
		"score" : 1
	},
	"inputStage" : {
		"stage" : "SORT_KEY_GENERATOR",
		"inputStage" : {
			"stage" : "COLLSCAN",  // 全集合扫描
			"direction" : "forward"
		}
	}
}

我们也可以强制使用稀疏索引:

// hint 明确指定索引
db.s11.find().sort({"score": 1}).hint({"score": 1})
{ "_id" : ObjectId("600fb8d164bc3da87653e9f4"), "name" : "wangkai", "score" : 92 }
{ "_id" : ObjectId("600fb8d164bc3da87653e9f3"), "name" : "likai", "score" : 95 }

db.s11.find().hint({"score": 1})  // 跟上一条语句的返回结果一致
{ "_id" : ObjectId("600fb8d164bc3da87653e9f4"), "name" : "wangkai", "score" : 92 }
{ "_id" : ObjectId("600fb8d164bc3da87653e9f3"), "name" : "likai", "score" : 95 }

db.s11.find().hint({"score": 1}).explain()["queryPlanner"]["winningPlan"]
{
	"stage" : "FETCH",
	"inputStage" : {
		"stage" : "IXSCAN",
		"keyPattern" : {
			"score" : 1
		},
		"indexName" : "score_1",
		"isMultiKey" : false,
		"multiKeyPaths" : {
			"score" : [ ]
		},
		"isUnique" : false,
		"isSparse" : true,
		"isPartial" : false,
		"indexVersion" : 2,
		"direction" : "forward",
		"indexBounds" : {
			"score" : [
				"[MinKey, MaxKey]"
			]
		}
	}
}

// 当然,如果你要对文档进行计数时,不要使用 hint 和稀疏索引
db.s11.count()
3
db.s11.find().hint({"score": 1}).count()
2

部分索引

部分索引(Partial Indexes)是MongoDB3.2版本中的新功能,也叫做局部索引。

部分索引仅索引集合中符合指定过滤器表达式的文档,且由于部分索引是集合的子集,所以部分索引具有较低的存储需求,并降低了索引创建和维护的性能成本。部分索引通过指定过滤条件来创建,可以为MongoDB支持的所有索引类型使用部分索引。

部分索引中常用的过滤器表达式

  • 等式表达式,$eq
  • $exists
  • 大于小于等于系列
  • $type
  • and

创建部分索引

// 准备数据
db.s12.insertMany([
    {"name": "zhangkai", "score": 85},
    {"name": "likai", "score": 95},
    {"name": "wangkai", "score": 92},
    {"name": "zhangkai1", "score": 87},
    {"name": "likai1", "score": 97},
    {"name": "wangkai1", "score": 99},
    {"name": "zhangkai2", "score": 25},
    {"name": "likai2", "score": 45},
    {"name": "wangkai2", "score": 32},
])


// 创建部分索引
db.s12.createIndex(
    {"score": 1}, 
    {
        "partialFilterExpression": {
            "score":{
                "$gte": 60
            }
        }
})

// 只有当查询条件大于等于60的时候,才走部分索引
db.s12.find({"score": {"$gte": 60}}).explain()["queryPlanner"]["winningPlan"]
{
	"stage" : "FETCH",
	"inputStage" : {
		"stage" : "IXSCAN",  // 走了索引
		"keyPattern" : {
			"score" : 1
		},
		"indexName" : "score_1",
		"isMultiKey" : false,
		"multiKeyPaths" : {
			"score" : [ ]
		},
		"isUnique" : false,
		"isSparse" : false,
		"isPartial" : true,
		"indexVersion" : 2,
		"direction" : "forward",
		"indexBounds" : {
			"score" : [
				"[60.0, inf.0]"
			]
		}
	}
}

// 下面示例,不会走部分索引
db.s12.find({"score": {"$gt": 59}}).explain()["queryPlanner"]["winningPlan"]
{
	"stage" : "COLLSCAN",  // 全集合扫描
	"filter" : {
		"score" : {
			"$gt" : 59
		}
	},
	"direction" : "forward"
}

再来看部分索引和唯一索引同时使用时的一些现象:

db.s12.remove({})
db.s12.insertMany([
    {"name": "zhangkai", "score": 85},
    {"name": "likai", "score": 95}
])


db.s12.createIndex(
	{"name": 1},
    {
        "unique": true,
        "partialFilterExpression": {
            "score": {
                "$gt": 60
            }
        }
    }
)



// 插入 name 值相同的文档, 报错,不允许插入
db.s12.insert({"name": "zhangkai", "score": 77})  // "errmsg" : "E11000 duplicate key error collection: t1.s12 index: name_1 dup key: { : \"zhangkai\" }"


// 以下几种情况允许插入
db.s12.insertMany([
    {"name": "zhangkai", "score": 30},  // name 值重复,score 值小于部分索引限制
    {"name": "zhangkai", "score": null},  // name 值重复,score 值为 null
    {"name": "zhaokai"},  // 忽略 score 字段
])

// 文档已存在,再插入就报错
db.s12.insert({"name": "zhangkai", "score": 85})  // score值大于部分索引限制,校验 name 唯一性

// score 值小于部分索引,允许插入重复 name 值
db.s12.insert({"name": "zhaokai", "score": 30})  

// name 值不重复,score值重复,允许插入
db.s12.insert({"name": "sunkai", "score": 70})  

由上例的测试结果可以发现,当对唯一索引添加部分索引时,插入时检查部分索引字段的唯一性,什么意思呢?如上例的索引,它只对于score值大于等于60的文档,才去校验name的唯一性,同时允许姓名不同,score值相同的文档插入。

部分索引和稀疏索对比

部分索引主要是针对那些满足条件的文档(非字段缺失)创建索引,比稀疏索引提供了更具有表现力。

稀疏索引是文档上某些字段的存在与否,存在则为其创建索引,否则该文档没有索引键。

覆盖查询

再来了解一下MongoDB中的覆盖查询。

覆盖查询是一种查询现象。

当查询条件和查询的投影仅包含索引字段时,MongoDB会直接从索引中返回结果,而不扫描任何文档或者将文档带入内存,这样的查询性能非常高。

如上图,如果对score字段建立了索引,查询时只返回score字段,这就会触发覆盖索引,即查询结果来自于索引,而不走文档集。

首先是无索引情况:

// 文档长这样
db.s7.find( {"score":{ "$gt": 80 }}).limit(2)
{ "_id" : 28, "name" : "邓桂芳", "age" : 10, "info" : { "address" : "海口", "tel" : "13465340126" }, "score" : 81 }
{ "_id" : 31, "name" : "耿涛", "age" : 20, "info" : { "address" : "关岭", "tel" : "14581415750" }, "score" : 81 }

// 没有索引,没有投影,一个普通的查询,我们来看它的执行计划
db.s7.find( {"score":{ "$gt": 80 }}).explain()["queryPlanner"]["winningPlan"]
{
	"stage" : "COLLSCAN",  // 表示全集合扫描
	"filter" : {
		"score" : {
			"$gt" : 80
		}
	},
	"direction" : "forward"
}

// 没有索引,但有投影
db.s7.find( {"score":{ "$gt": 80 }}, {"score": 1, "_id": 0} ).explain()["queryPlanner"]["winningPlan"]
{
	"stage" : "PROJECTION",  // 对 score 字段做了投影后的 stage 状态
	"transformBy" : {
		"score" : 1,
		"_id" : 0
	},
	"inputStage" : {
		"stage" : "COLLSCAN",  // 还是全集合扫描
		"filter" : {
			"score" : {
				"$gt" : 80
			}
		},
		"direction" : "forward"
	}
}

现在添加索引,再来看两个查询计划:

// 添加索引
db.s7.createIndex({"score": 1})

// 加上索引后,但没有投影
db.s7.find( {"score":{ "$gt": 80 }}).explain()["queryPlanner"]["winningPlan"]
{
	"stage" : "FETCH",  // 根据索引检索指定的文档
	"inputStage" : {
		"stage" : "IXSCAN",  // 索引扫描
		"keyPattern" : {
			"score" : 1
		},
		"indexName" : "score_1",
		"isMultiKey" : false,
		"multiKeyPaths" : {
			"score" : [ ]
		},
		"isUnique" : false,
		"isSparse" : false,
		"isPartial" : false,
		"indexVersion" : 2,
		"direction" : "forward",
		"indexBounds" : {
			"score" : [
				"(80.0, inf.0]"
			]
		}
	}
}

// 有索引和对 score 字段做投影后的情况,这种情况就是覆盖查询
db.s7.find( {"score":{ "$gt": 80 }}, {"score": 1, "_id": 0} ).explain()["queryPlanner"]["winningPlan"]
{
	"stage" : "PROJECTION",  // 对 score 字段做了投影后的 stage 状态
	"transformBy" : {
		"score" : 1,
		"_id" : 0
	},
	"inputStage" : {
		"stage" : "IXSCAN",
		"keyPattern" : {
			"score" : 1
		},
		"indexName" : "score_1",
		"isMultiKey" : false,
		"multiKeyPaths" : {
			"score" : [ ]
		},
		"isUnique" : false,
		"isSparse" : false,
		"isPartial" : false,
		"indexVersion" : 2,
		"direction" : "forward",
		"indexBounds" : {
			"score" : [
				"(80.0, inf.0]"
			]
		}
	}
}

索引的注意事项

  1. MongoDB的索引是存储在运行内存(RAM)中的,所以必须确保索引的大小不超过内存的限制。

    如果索引的大小超过了运行内存的限制,MongoDB会删除一些索引,这将导致性能下降。

  2. MongoDB的索引在部分查询条件下是不会生效的。

    • 正则表达式及非操作符,如 $nin,$not , 等。
    • 算术运算符,如 $mod, 等。
    • $where自定义查询函数。
    • ...
  3. 索引会在写入数据(添加、更新和删除)时重排,如果项目如果是写多读少,则建议少使用或者不要使用索引。

  4. 一个集合中索引数量不能超过64个。

  5. 索引名的长度不能超过128个字符。

  6. 一个复合索引最多可以有31个字段。

  7. mongodb索引统一在system.indexes集合中管理。这个集合只能通过createIndexdropIndexes来操作。


that's all, see also:

MongoDB 部分索引(Partial Indexes) | MongoDB TTL索引的使用 | MongoDB 唯一索引 | MongoDB 复合索引 | MongoDB - 上限集合 | MongoDB 多键索引

posted @ 2021-01-26 17:26  听雨危楼  阅读(1628)  评论(0编辑  收藏  举报