Mongodb索引

MongoDB索引主要内容

  • MongoDB索引的概念&机制

  • MongoDB索引的类型

  • MongoDB索引的管理


索引的概念

数据库索引是对数据表中一列或多列的值进行排序的一种数据结构,使用索引可以快速访问数据库表中的特定信息。

数据库索引的功能类似于书籍的索引,书籍有了索引就不需要翻查整本书。于此类似,在进行查询时,数据库会首先在索引中查找,找到相应的条目后,就可以直接跳转到目标文档的位置

MongoDB索引几乎与关系型数据库的索引一样,绝大数优化关系型数据库索引的技巧同样适用于MongoDB。


索引的机制

1
2
3
4
5
6
// db.users.find()
{ "_id": ObjectId("5ab06959ea72f7b2089e1225"), "name: "jack", "score": 19 }
{ "_id": ObjectId("5ab06959ea72f7b2089e1226"), "name: "tom", "score": 18 }
{ "_id": ObjectId("5ab06959ea72f7b2089e1227"), "name: "liang", "score": 30 }
{ "_id": ObjectId("5ab06959ea72f7b2089e1228"), "name: "rose", "score": 30 }
{ "_id": ObjectId("5ab06959ea72f7b2089e1229"), "name: "admin", "score": 50 }

当你往某各个集合插入多个文档后,每个文档在经过底层的存储引擎持久化后,会有一个位置信息,通过这个位置信息,就能从存储引擎里读出该文档。比如mmapv1引擎里,位置信息是『文件id + 文件内offset 』, 在wiredtiger存储引擎(一个KV存储引擎)里,位置信息是wiredtiger在存储文档时生成的一个key,通过这个key能访问到对应的文档;为方便介绍,统一用pos(position的缩写)来代表位置信息。

在users集合里有5个文档,假设其存储后位置信息如下:

位置信息 文档
pos1 {“name: “jack”, “score”: 19 }
pos2 {“name: “tom”, “score”: 18 }
pos3 {“name: “liang”, “score”: 30 }
pos4 {“name: “rose”, “score”: 30 }
pos5 {“name: “admin”, “score”: 50 }

假设现在有个查询 db.users.find({ score: 30 }), 查询所有成绩为30分的人,这时需要遍历所有的文档(『全表扫描』),根据位置信息读出文档,对比score字段是否为30。当然如果只有5个文档,全表扫描的开销并不大,但如果集合文档数量到百万、甚至千万上亿的时候,对集合进行全表扫描开销是非常大的,一个查询耗费数十秒甚至几分钟都有可能。

如果想加速 db.users.find({ score: 30 }),就可以考虑对users表的score字段建立索引。

1
db.uses.createIndex({ score: 1 })

建立索引后,MongoDB会额外存储一份按score字段升序排序的索引数据,索引结构类似如下,索引通常采用类似btree的结构持久化存储,以保证从索引里快速(O(logN)的时间复杂度)找出某个score值对应的位置信息,然后根据位置信息就能读取出对应的文档。

索引本质上是树(多路平衡查找树)🌲,最小的值在最左边的叶子🍃上,最大的值在最右边的叶子🍃上。

SCORE 位置信息
18 pos2
19 pos1
30 pos3
30 pos4
50 pos5

索引的类型

MongoDB提供了多种类型的索引,功能十分强大,其类型如下:

类型 说明 作用
Single Filed 单字段索引 在普通字段,自文档以及子文档的某个字段上建立的索引
Compound Index 复合索引 同时在多个字段上建立的索引
Multikey Index 多键索引 对数组建立的索引
Geospatial Index 地理空间索引 对地理位置型数据建立的索引(支持球面和平面)
Text Index 全文索引 对每一个词建立索引,支持全文索引
Hashed Index 哈希索引 索引中存储的是被索引键的哈希值

MongoDB索引的类型 - 单字段索引

MongoDB可以在单个字段上建立索引,字段可以是普通字段、整个子文档以及子文档的某个字段。

1
2
3
db.users.createIndex({ 'name': 1 })
db.users.createIndex({ 'bind': 1 })
db.users.createIndex({ 'bind.Weixin.uid': 1 })

_id索引是系统默认创建的单字段升序且具有唯一性的索引,每个集合的文档都会包含该字段,不能被删除,默认值是ObjectId类型。


MongoDB索引的类型 - 复合索引

创建复合索引:

1
2
db.collection.createIndex( { <field1>: <type>, <field2>: <type2>, ... } )
db.users.createIndex({ userid: 1, score: -1 }) // 按照userid升序,score降序

索引字段排序优化:

1
2
3
// 上面创建的索引支持以下两种数据查询排序
db.users.find().sort({ userid: 1, score: -1 })
db.users.find().sort({ userid: -1, score: 1 })

复合索引也支持前缀匹配,相当于创建了userid索引。

使用复合索引,尽量使用覆盖索引查询。覆盖索引的条件:

  • 所有的查询字段是索引的一部分

  • 所有的查询返回字段在同一个索引中

1
db.users.find({}, { userid: 1, score: 1, _id: -1 })

只查询索引就完成了本次查询,不用根据索引检索文档,可极大提高查询效率。


MongoDB索引的类型 - 多键索引

多键索引是对数组类型建立的索引,对数组建立的索引,实际上是对数组的每个元素建立索引,而不是对数组本身建立索引

1
2
3
4
5
6
7
8
9
let book1 = { name: '大主宰', tags: ['玄幻', '奇幻'] }
let book2 = { name: '星辰变', tags: ['奇幻', '仙侠'] }

db.books.createIndex({ tags: 1 })

// 创建后索引大致如下:
玄幻 => [book1]
奇幻 => [book1, book2]
仙侠 => [book2]

关于多键索引需要注意的几个问题:

一个索引中的数组字段最多只能有一个。 由于复合键的笛卡尔积中的每一个值都要被索引,如果支持多个数组,笛卡尔积会很大,索引条目会爆炸式增长。如果数组A有m个元素,数组B有n个元素,那么在A与B字段上建立索引,将产生m*n个索引条目。

eg: {a: [1, 2], b: [1, 2]} a和b都是数组类型,无法创建这样的索引:
{a: 1, b:1}

  • 当数组元素是文档类型时,可以为文档的某个字段建立多键索引
1
2
3
4
5
6
7
8
9
10
{
"_id": ObjectId('5aad3aaf93cd493421f17088'),
"title": "推荐书单",
"books": [
{
"author": "dillon"
}
]
}
db.list.createIndex({ "books.author": 1 })

MongoDB索引的类型 - 全文本索引

MongoDB支持在文档中搜索文本,在需要搜索的文档字段上创建全文本索引。
创建任何一种索引的开销都比较大,而创建全文本索引的成本更高。MongoDB在字符串上创建全文本索引,对字符串进行分解,分词,并且保存到一些地方。

Note:

  • 一个文档集合最多只能创建一个文本索引

  • 可以在文档多个字段上创建文本索引

  • 可以在复合索引中创建文本索引

todo 栗子🌰


MongoDB索引的类型 - 地理空间索引索引

  • 2dsphere Indexes (球面)

  • 2d Indexes (平面)

  • geoHaystack Indexes

todo 栗子🌰


MongoDB索引的类型 - 哈希索引

哈希索引项中存储的是索引键的哈希值,哈希索引只支持等值查询,不支持范围查找。

1
db.users.createIndex({ name: 'hashed' })

哈希索引主要用于分片的集合上,可以作为片键来使用,能够将数据比较均匀的分散存储在各个分片上。


MongoDB索引的属性

  • 唯一(Unique)索引

  • 稀疏(Sparse)索引

  • TTL(Time To Live)索引

  • 部分(Partial)索引

  • Case Insensitive Indexes


MongoDB索引 - 唯一索引

唯一索引可以确保集合的每一个文档的索引字段都有唯一的值,不会出现重复值。

创建唯一索引:

1
db.users.createIndex({ nickname: 1 }, { unique: true })

非空集合上创建唯一索引可能会失败,因为集合中索引字段已经存在重复值,这时可以使用dropDups: true选项来删除重复的数据:

1
db.users.createIndex({ nickname: 1 }, { unique: true, dropDups: true })

Note:

  1. _id

  2. 复合索引

  3. null

  4. 不能为哈希索引指定唯一属性


MongoDB索引 - 稀疏索引

稀疏索引指的是只为索引字段存在的文档建立索引,即使索引字段的值为null,但不会为索引字段不存在的文档创建索引。

创建稀疏索引:

1
db.users.createIndex({ age: 1 }, { spare: true })

Note:

  • 对应复合索引,只要有一个索引字段存在,就会为该文档建立索引

  • 默认情况下,地理空间索引和全文索引都是稀疏索引


MongoDB索引 - TTL索引

TTL(Time-To-Live)索引可以为文档设置一个超时时间,当达到预设值的时间后,该文档会被数据库自动删除。

创建ttl索引:

1
db.errlogs.createIndex({ created: 1 }, { expireAfterSeconds: 60 * 60 })

Note:

  • TTL索引只能建立在单独字段上,在复合索引上无法指定TTL属性

  • 在_id字段无法使用TTL索引

  • 在固定集合(Capped collection)上无法使用TTL索引

  • 创建TTL索引后,MongoDB会有一个TTL后台线程来管理文档,当达到超时时间时会将文档删除

  • 使用collMod修改TTL索引的超时时间,eg: db.runCommand({collMod: 集合名, index: {keyPattern: {索引名}, expireAfterSeconds: 时间}})

  • TTL索引不能保证达到过期时间时,立即将文档删除,中间可能会有一定的延迟

  • 在复制集上建立的TTL索引,TTL后台线程只会运行在主节点上

todo🌰

MongoDB索引 - 部分索引

部分索引仅索引符合指定过滤器表达式的集合中的文档。
通过索引集合中的文档的子集,部分索引具有较低的存储需求并且降低了索引创建和维护的性能成本。

todo🌰

ref: https://docs.mongodb.com/manual/core/index-partial/

MongoDB索引 - Case Insensitive索引

Case insensitive indexes support queries that perform string comparisons without regard for case.

不区分大小写的索引支持执行字符串比较而不考虑大小写的查询。

创建Case Insensitive索引需要指定参数collation:

1
2
3
4
5
6
db.collection.createIndex( { "key" : 1 },
{ collation: {
locale : <locale>,
strength : <strength>
}
} )

其中,locale指定语言,strength指定比较级别。

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
db.createCollection("fruit")
db.fruit.createIndex( { type: 1},
{ collation: { locale: 'en', strength: 2 } } )

db.fruit.insert( [ { type: "apple" },
{ type: "Apple" },
{ type: "APPLE" } ] )

db.fruit.find( { type: "apple" } ) // does not use index, finds one result

db.fruit.find( { type: "apple" } ).collation( { locale: 'en', strength: 2 } )
// uses the index, finds three results

db.fruit.find( { type: "apple" } ).collation( { locale: 'en', strength: 1 } )
// does not use the index, finds three results

ref: https://docs.mongodb.com/manual/core/index-case-insensitive/


创建索引的参数

索引优化器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// db.books.find({majorCate: '玄幻'}, { majorCate: 1}).explain('executionStats')

{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "gleeman.books",
"indexFilterSet" : false,
"parsedQuery" : {
"majorCate" : {
"$eq" : "玄幻"
}
},
"winningPlan" : { // 最优执行计划
"stage" : "PROJECTION", // 制定返回字段时是projection
"transformBy" : {
"majorCate" : 1.0,
"minorCate" : 1.0
},
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN", // 根据索引查询
"keyPattern" : {
"majorCate" : 1
},
"indexName" : "majorCate_1",
"isMultiKey" : false, // 多键索引为true
"direction" : "forward",
"indexBounds" : {
"majorCate" : [
"[\"玄幻\", \"玄幻\"]"
]
}
}
}
},
"rejectedPlans" : [
{
"stage" : "PROJECTION",
"transformBy" : {
"majorCate" : 1.0,
"minorCate" : 1.0
},
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"majorCate" : 1,
"minorCate" : 1
},
"indexName" : "majorCate_1_minorCate_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"majorCate" : [
"[\"玄幻\", \"玄幻\"]"
],
"minorCate" : [
"[MinKey, MaxKey]" // 因为只指定了majorCate, 所以minorCate的索引范围是[Minkey, MaxKey]
]
}
}
}
}
]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 513063,
"executionTimeMillis" : 14417,
"totalKeysExamined" : 513063,
"totalDocsExamined" : 513063,
"executionStages" : {
"stage" : "PROJECTION",
"nReturned" : 513063,
"executionTimeMillisEstimate" : 13711,
"works" : 513064,
"advanced" : 513063,
"needTime" : 0,
"needFetch" : 0,
"saveState" : 4009,
"restoreState" : 4009,
"isEOF" : 1,
"invalidates" : 0,
"transformBy" : {
"majorCate" : 1.0,
"minorCate" : 1.0
},
"inputStage" : {
"stage" : "FETCH",
"nReturned" : 513063,
"executionTimeMillisEstimate" : 11223,
"works" : 513064,
"advanced" : 513063,
"needTime" : 0,
"needFetch" : 0,
"saveState" : 4009,
"restoreState" : 4009,
"isEOF" : 1,
"invalidates" : 0,
"docsExamined" : 513063,
"alreadyHasObj" : 0,
"inputStage" : {
"stage" : "IXSCAN",
"nReturned" : 513063,
"executionTimeMillisEstimate" : 560,
"works" : 513064,
"advanced" : 513063,
"needTime" : 0,
"needFetch" : 0,
"saveState" : 4009,
"restoreState" : 4009,
"isEOF" : 1,
"invalidates" : 0,
"keyPattern" : {
"majorCate" : 1
},
"indexName" : "majorCate_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"majorCate" : [
"[\"玄幻\", \"玄幻\"]"
]
},
"keysExamined" : 513063,
"dupsTested" : 0,
"dupsDropped" : 0,
"seenInvalidated" : 0,
"matchTested" : 0
}
}
}
},
"serverInfo" : {
"host" : "database28",
"port" : 27017,
"version" : "3.0.11",
"gitVersion" : "48f8b49dc30cc2485c6c1f3db31b723258fcbf39"
},
"ok" : 1.0
}

stage:

COLLSCAN 全表扫描(没有索引的时候)
IXSCAN 索引扫描
FETCH 根据索引检索文档
SORT 在内存中排序(大数据浪费性能)
TEXT 全文索引扫描
PROJECTION 限定返回字段时


参考:
MongoDB索引: https://docs.mongodb.com/manual/indexes/
MongoDB索引原理: http://www.mongoing.com/archives/2797