前言
在查询操作中,如果没有索引,[MongoDB](http://c.biancheng.net/mongodb/) 会扫描集合中的每个文档,以选择与查询语句匹配的文档。如果查询条件带有索引,MongoDB 将扫描索引, 通过索引确定要查询的部分文档,而非直接对全部文档进行扫描
索引可以提升文档的查询速度,但建立索引的过程需要使用计算与存储资源,在已经建立索引的前提下,插入新的文档会引起索引顺序的重排。
MongoDB 的索引是基于 B-tree [数据结构](http://c.biancheng.net/data_structure/)(MySQL是B+Tree)及对应算法形成的。树索引存储特定字段或字段集的值,按字段值排序。索引条目的排序支持有效的等式匹配和基于范围的查询操作。
下图所示的过程说明了使用索引选择和排序匹配文档的查询过程。
MongoDB 在创建集合时,会默认在 _id 字段上创建唯一索引。该索引可防止客户端插入具有相同字段的两个文档,_id 字段上的索引不能被删除。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/21500184/1658819268247-e59319cb-d4aa-48a6-b846-4d4d7f1d7aae.png#clientId=u38cf9b16-3ce3-4&from=paste&height=348&id=u3b26b712&originHeight=344&originWidth=750&originalType=binary&ratio=1&rotation=0&showTitle=false&size=78882&status=done&style=none&taskId=u95608925-c9dc-47cd-85ad-f72c855d2b4&title=&width=759)
1.索引的优缺点
1.1 优点
- 查询速率很快
- 大大减少了服务器需要扫描的数据量
- 索引可以将随机io转换为顺序io
1.2 缺点
- 降低插入速度
- 浪费存储空间,删除空间不会释放字段(需要通过命令来整理碎片,这个过错比较缓慢)导致mongodb占用空间会比较大
- 不适当的查询语句,会导致并未使用索引
1.3 最大范围:
- 集合不能超过64个索引
- 索引名的长度不能超过125个字符
- 一个复合索引最多可以有31字段索引
2. 知识点
- mongo索引是有方向的,1 代表升序, -1代表降序
不同于MySQL的索引, mongo的索引是有方向的, value代表了索引的方向, 这个特性在排序的使用很好用。1 代表升序, -1代表降序
- mongo在创建集合时,默认_id为唯一索引,该索引可防止客户端插入具有相同字段的两个文档,_id 字段上的索引不能被删除。
3.无法使用索引的操作
- $where和$exists无法使用索引,
$exists`操作, 会遍历每个文档,以确定字段是否存在, `$where
会同样的遍历每个文档。 - 类似MySQL, 在mongo中使用取反的查询操作, 也会导致索引利用效率低下, 或者不会使用索引, 例如
$ne`, 而`$not
虽然有时能够使用索引, 但是通常它并不知道该如何利用索引。 $nin
, 总是扫描整个集合。
4. 索引类型
MongoDB 中索引的类型大致包含单键索引、复合索引、多键值索引、地理索引、全文索引、 散列索引等,下面简单介绍各类索引的用法。<br />其中地理索引、全文索引、 散列索引,使用场景特殊,如需使用请自行查找相关文档
4.1 单键索引
单字段索引和排序操作,索引键的排序顺序(即升序或降序)无关紧要,因为 MongoDB 可以在任意方向上遍历索引。
- value: 1 为升序,-1 为降序
db.collection.createIndex({filedName: value})
4.2 复合索引
就是在创建索引的时候同时指定多个字段
注意事项
- 索引是区分方向的
db.users.createIndex({"name": 1, "age": 1})
与db.users.createIndex({"name": 1, "age": -1})
是两个不同的索引。只有要针对多个字段进行排序时, 索引的方向才是重要的, mongo会自动的翻转索引, 也就是说{"name": 1}
与{"name": -1}
在使用时是一致的。{"name": 1, "age": 1}
与{"name": -1, "age": -1}
也是一致的。 - 索引字段要有序
为了能够更加有效的利用索引, 用于精确匹配的字段应该放在索引的前面, 范围字段放在后面, 这样mongo就能利用精确匹配过滤掉大部分文档, 然后利用之后的索引去再次过滤结果。例如我们要查询name=user10&age>20
, 那么我们的索引应该是{"name": 1, "age": 1}
, 而不是{"age": 1, "name": 1}
- 隐式索引
如果我们创建一个复合索引{"name": 1, "age": 1, "created": -1}
, 当利用{"name": 1}
进行排序的时候, 也能够使用前面创建的索引, 注意, 必须是从左往右依次匹配才行, 也就是说{"age": 1}
, 这种是不会用到索引的。4.3 多键值索引
若要为包含数组的字段建立索引,MongoDB 会为数组中的每个元素创建索引键。这些多键值索引支持对数组字段的高效查询
需要注意的是,如果集合中包含多个待索引字段是数组,则无法创建复合多键索引。
以下示例代码展示插入文档,并创建多键值索引:
db.users.insert ({item : "ABC", ratings: [ 2, 5, 9 ]})
db.users.createIndex({ratings:1})
db.users.find({ratings:2}).explain()
4.4 唯一索引
类似MySQL的唯一约束, mongo也是具有的, 用来保证一个集合内相同字段值的唯一性
db.users.createIndex({"name": 1}, {"unique": true, "name": index_name})
如此我们就建立了一个唯一索引, 当然类似MySQL 的联合唯一索引mongo也是有的, 只需要指定多个字段即可。通过name
属性, 能够手动指定索引的名字。
4.5 TTL索引
db.collection.createIndex({"name": 1}, {"expireAfterSeconds": 10*60})
如此就创建了一个TTL索引, mongo会每分钟检查一次索引, 并删除过期的文档。这个索引可以用来排序和搜索。
5. 索引操作
5.1 删除索引
//删除一个索引
db.collection.dropIndex("index_name")
//删除所有索引
db.collection.dropIndexes()
5.2使用指定的索引
利用mongo的hint()
操作可以指定此次查询使用的索引。
5.3 使用索引
类似于MySQL, mongo也有explain方法, 去查看索引的使用情况
db.users.find({"name": "user100"}).explain()
就能查看是否使用了索引, 以及其他的一些详细信息, 包括, 使用的索引, 扫描的文档数据, 结果的数量, 查询用时等等。
5.3.1 查看现有索引
若要返回集合上所有索引的列表,则需使用驱动程序的 db.collection.getlndexes() 方法或类似方法。
例如,可使用如下方法查看 records 集合上的所有索引:
db.records.getIndexes()
5.3.2 修改索引
若要修改现有索引,则需要删除现有索引并重新创建索引。
5.3.3 产看索引占用的空间
- is_detail:可选参数,传入除0或false外的任意数据,都会显示该集合中每个索引的大小及总大小。如果传入0或false则只显示该集合中所有索引的总大小。默认值为false。
db.collection.totalIndexSize([is_detail])
6. explain() 函数,可以查看查询过程
可以看到queryPlanner.winningPlan.stage = IXSCAN即使走了索引
查询具体的执行时间:关注输出的如下数值:executionStats.executionTimeMillis
{
"queryPlanner": {
# 查询计划
"plannerVersion": NumberInt("1"), # 计划版本
"namespace": "db_name.collection_name", # 命名空间,作用于哪个库的哪个集合
"indexFilterSet": false, # 是否对查询使用索引过滤
"parsedQuery": {
# 解析查询条件
"index_key_name": {
"$eq": 1
}
},
"queryHash": "6ACB91B3", # 仅对查询条件进行hash的16进制字符串,帮助识别相同查询条件的 其他查询操作或写操作
"planCacheKey": "ACADC259", # 和查询关联的计划缓存的hash键
"winningPlan": {
# 查询优化器 选择的最优执行计划,此计划包含多个 树状结构的子阶段(一个查询计划需要多个阶段来完成);
"stage": "FETCH", # 父阶段;查询方式
"inputStage": {
# 子阶段
"stage": "IXSCAN", # 子阶段的查询方式
"keyPattern": {
# 索引模式
"index_key_name": 1
},
"indexName": "index_key_name_1", # 索引名称
"isMultiKey": false, # 是否是复合索引
"multiKeyPaths": {
# 复合索引路径
"index_key_name": []
},
"isUnique": true, # 是否是唯一索引
"isSparse": false, # 是否是稀疏索引
"isPartial": false, # 是否是部分索引
"indexVersion": NumberInt("2"), # 索引版本
"direction": "forward", # 索引方向
"indexBounds": {
# 索引查询的范围边界
"index_key_name": [ # 创建索引的key
"[1.0, 1.0]" # 边界范围
]
}
}
},
"rejectedPlans": [] # 查询又花钱 拒绝的执行计划
},
"executionStats": {
# 详细的执行统计信息
"executionSuccess": true, # 是否执行成功
"nReturned": NumberInt("0"), # 符合查询条件的文档个数
"executionTimeMillis": NumberInt("0"), # 选择某个查询计划和执行查询 所耗费的总时间(毫秒)
"totalKeysExamined": NumberInt("0"), # 扫描的索引总行数
"totalDocsExamined": NumberInt("0"), # 扫描的文档总次数(即使同一个文档如果被扫描2次,则此值为2),常见于 stage为 COLLSCAN/FETCH
"executionStages": {
# 用树状形式 描述 详细的执行计划
"stage": "FETCH", # 查询方式
"nReturned": NumberInt("0"),
"executionTimeMillisEstimate": NumberInt("0"), # 估计执行时间(毫秒)
"works": NumberInt("1"), # 指定查询执行阶段执行的“工作单元”的数量。查询执行将其工作划分为小单元。
# “工作单元”可能包括检查单个索引键、从集合中获取单个文档、对单个文档应用投影或进行内部簿记
"advanced": NumberInt("0"), # 由这一阶段返回到它的父阶段的中间结果或高级结果的数量。
"needTime": NumberInt("0"), # 未将中间结果提前到其父阶段的工作循环数
"needYield": NumberInt("0"), # 为了让写操作执行,而让出读锁的次数
"saveState": NumberInt("0"), # 查询阶段暂停处理并保存其当前执行状态的次数,例如准备放弃其锁
"restoreState": NumberInt("0"), # 查询阶段恢复已保存的执行状态的次数,例如,在恢复以前生成的锁之后。
"isEOF": NumberInt("1"), # 执行阶段是否已到达最后一个; 1:是 0:不是
"docsExamined": NumberInt("0"), # 扫描文档总次数
"alreadyHasObj": NumberInt("0"),
"inputStage": {
"stage": "IXSCAN",
"nReturned": NumberInt("0"),
"executionTimeMillisEstimate": NumberInt("0"),
"works": NumberInt("1"),
"advanced": NumberInt("0"),
"needTime": NumberInt("0"),
"needYield": NumberInt("0"),
"saveState": NumberInt("0"),
"restoreState": NumberInt("0"),
"isEOF": NumberInt("1"),
"keyPattern": {
"index_key_name": 1
},
"indexName": "index_key_name_1",
"isMultiKey": false,
"multiKeyPaths": {
"index_key_name": []
},
"isUnique": true,
"isSparse": false,
"isPartial": false,
"indexVersion": NumberInt("2"),
"direction": "forward",
"indexBounds": {
"index_key_name": [
"[1.0, 1.0]"
]
},
"keysExamined": NumberInt("0"), # 通过索引扫描的文档总个数
"seeks": NumberInt("1"), # 为了完成索引扫描,必须将索引游标搜索到新位置的次数。
"dupsTested": NumberInt("0"),
"dupsDropped": NumberInt("0")
}
},
"allPlansExecution": [] # 在计划选择阶段,获胜计划和被拒绝计划的部分执行信息
},
"serverInfo": {
# mongo服务信息
"host": "aa8f4be",
"port": NumberInt("27010"),
"version": "4.2.0",
"gitVersion": "a4b751dcf51dd249c5865812b390cfd"
},
"ok": 1
}
queryPlanner.winningPlan.stage字段含义
字段 | 值 | 含义 |
---|---|---|
stage | COLLSCAN | 全表扫描 |
IXSCAN | 索引扫描 | |
FETCH | 根据索引去检索指定文档 | |
SHARD_MERGE | 将各个分片返回数据进行合并 | |
SHARDING_FILTER | 分片过滤 | |
SORT | 表明在内存中进行了排序 | |
LIMIT | 使用limit限制返回数 | |
SKIP | 使用skip进行跳过 | |
IDHACK | 针对_id进行查询 | |
nReturned 实际返回的文档个数
totalKeysExamined 扫描的索引总行数
totalDocsExamined 扫描的文档总行数
executionTimeMillis 执行耗费时间(毫秒)
7.例子
加3000W数据
for (var i = 0; i < 3000000; i++) {
var vip = false;
if (i % 2 === 0) {
vip = true;
}
var remark = "这是一条测试数据,这是第" + i + "条";
db.getCollection("users").insert({
name: "long",
age: i,
remark,
vip,
create_time: new Date()
});
}
db.users.find({
age:"499021"}).explain(1)
未增加索引前:executionTimeMillis执行时间为937毫秒
{
"explainVersion": "1",
"queryPlanner": {
"namespace": "test_three.users",
"indexFilterSet": false,
"parsedQuery": {
"age": {
"$eq": "499021"
}
},
"maxIndexedOrSolutionsReached": false,
"maxIndexedAndSolutionsReached": false,
"maxScansToExplodeReached": false,
"winningPlan": {
"stage": "COLLSCAN",
"filter": {
"age": {
"$eq": "499021"
}
},
"direction": "forward"
},
"rejectedPlans": [ ]
},
"executionStats": {
"executionSuccess": true,
"nReturned": NumberInt("0"),
"executionTimeMillis": NumberInt("937"),
"totalKeysExamined": NumberInt("0"),
"totalDocsExamined": NumberInt("1999999"),
"executionStages": {
"stage": "COLLSCAN",
"filter": {
"age": {
"$eq": "499021"
}
},
"nReturned": NumberInt("0"),
"executionTimeMillisEstimate": NumberInt("7"),
"works": NumberInt("2000001"),
"advanced": NumberInt("0"),
"needTime": NumberInt("2000000"),
"needYield": NumberInt("0"),
"saveState": NumberInt("2000"),
"restoreState": NumberInt("2000"),
"isEOF": NumberInt("1"),
"direction": "forward",
"docsExamined": NumberInt("1999999")
},
"allPlansExecution": [ ]
},
"command": {
"find": "users",
"filter": {
"age": "499021"
},
"$db": "test_three"
},
"serverInfo": {
"host": "malongdeMBP.lan",
"port": NumberInt("27017"),
"version": "5.0.6",
"gitVersion": "212a8dbb47f07427dae194a9c75baec1d81d9259"
},
"serverParameters": {
"internalQueryFacetBufferSizeBytes": NumberInt("104857600"),
"internalQueryFacetMaxOutputDocSizeBytes": NumberInt("104857600"),
"internalLookupStageIntermediateDocumentMaxSizeBytes": NumberInt("104857600"),
"internalDocumentSourceGroupMaxMemoryBytes": NumberInt("104857600"),
"internalQueryMaxBlockingSortMemoryUsageBytes": NumberInt("104857600"),
"internalQueryProhibitBlockingMergeOnMongoS": NumberInt("0"),
"internalQueryMaxAddToSetBytes": NumberInt("104857600"),
"internalDocumentSourceSetWindowFieldsMaxMemoryBytes": NumberInt("104857600")
},
"ok": 1
}
增加索引后
// 1
{
"explainVersion": "1",
"queryPlanner": {
"namespace": "test_three.users",
"indexFilterSet": false,
"parsedQuery": {
"age": {
"$eq": 499021
}
},
"maxIndexedOrSolutionsReached": false,
"maxIndexedAndSolutionsReached": false,
"maxScansToExplodeReached": false,
"winningPlan": {
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"keyPattern": {
"age": 1
},
"indexName": "age_1",
"isMultiKey": false,
"multiKeyPaths": {
"age": [ ]
},
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": NumberInt("2"),
"direction": "forward",
"indexBounds": {
"age": [
"[499021.0, 499021.0]"
]
}
}
},
"rejectedPlans": [ ]
},
"executionStats": {
"executionSuccess": true,
"nReturned": NumberInt("1"),
"executionTimeMillis": NumberInt("0"),
"totalKeysExamined": NumberInt("1"),
"totalDocsExamined": NumberInt("1"),
"executionStages": {
"stage": "FETCH",
"nReturned": NumberInt("1"),
"executionTimeMillisEstimate": NumberInt("0"),
"works": NumberInt("2"),
"advanced": NumberInt("1"),
"needTime": NumberInt("0"),
"needYield": NumberInt("0"),
"saveState": NumberInt("0"),
"restoreState": NumberInt("0"),
"isEOF": NumberInt("1"),
"docsExamined": NumberInt("1"),
"alreadyHasObj": NumberInt("0"),
"inputStage": {
"stage": "IXSCAN",
"nReturned": NumberInt("1"),
"executionTimeMillisEstimate": NumberInt("0"),
"works": NumberInt("2"),
"advanced": NumberInt("1"),
"needTime": NumberInt("0"),
"needYield": NumberInt("0"),
"saveState": NumberInt("0"),
"restoreState": NumberInt("0"),
"isEOF": NumberInt("1"),
"keyPattern": {
"age": 1
},
"indexName": "age_1",
"isMultiKey": false,
"multiKeyPaths": {
"age": [ ]
},
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": NumberInt("2"),
"direction": "forward",
"indexBounds": {
"age": [
"[499021.0, 499021.0]"
]
},
"keysExamined": NumberInt("1"),
"seeks": NumberInt("1"),
"dupsTested": NumberInt("0"),
"dupsDropped": NumberInt("0")
}
},
"allPlansExecution": [ ]
},
"command": {
"find": "users",
"filter": {
"age": 499021
},
"$db": "test_three"
},
"serverInfo": {
"host": "malongdeMBP.lan",
"port": NumberInt("27017"),
"version": "5.0.6",
"gitVersion": "212a8dbb47f07427dae194a9c75baec1d81d9259"
},
"serverParameters": {
"internalQueryFacetBufferSizeBytes": NumberInt("104857600"),
"internalQueryFacetMaxOutputDocSizeBytes": NumberInt("104857600"),
"internalLookupStageIntermediateDocumentMaxSizeBytes": NumberInt("104857600"),
"internalDocumentSourceGroupMaxMemoryBytes": NumberInt("104857600"),
"internalQueryMaxBlockingSortMemoryUsageBytes": NumberInt("104857600"),
"internalQueryProhibitBlockingMergeOnMongoS": NumberInt("0"),
"internalQueryMaxAddToSetBytes": NumberInt("104857600"),
"internalDocumentSourceSetWindowFieldsMaxMemoryBytes": NumberInt("104857600")
},
"ok": 1
}
db.users.find({
age:{
$gt:499021}}).explain(1)
// 1
{
"explainVersion": "1",
"queryPlanner": {
"namespace": "test_three.users",
"indexFilterSet": false,
"parsedQuery": {
"age": {
"$gt": 499021
}
},
"maxIndexedOrSolutionsReached": false,
"maxIndexedAndSolutionsReached": false,
"maxScansToExplodeReached": false,
"winningPlan": {
"stage": "COLLSCAN",
"filter": {
"age": {
"$gt": 499021
}
},
"direction": "forward"
},
"rejectedPlans": [ ]
},
"executionStats": {
"executionSuccess": true,
"nReturned": NumberInt("1500977"),
"executionTimeMillis": NumberInt("766"),
"totalKeysExamined": NumberInt("0"),
"totalDocsExamined": NumberInt("1999999"),
"executionStages": {
"stage": "COLLSCAN",
"filter": {
"age": {
"$gt": 499021
}
},
"nReturned": NumberInt("1500977"),
"executionTimeMillisEstimate": NumberInt("4"),
"works": NumberInt("2000001"),
"advanced": NumberInt("1500977"),
"needTime": NumberInt("499023"),
"needYield": NumberInt("0"),
"saveState": NumberInt("2000"),
"restoreState": NumberInt("2000"),
"isEOF": NumberInt("1"),
"direction": "forward",
"docsExamined": NumberInt("1999999")
},
"allPlansExecution": [ ]
},
"command": {
"find": "users",
"filter": {
"age": {
"$gt": 499021
}
},
"$db": "test_three"
},
"serverInfo": {
"host": "malongdeMBP.lan",
"port": NumberInt("27017"),
"version": "5.0.6",
"gitVersion": "212a8dbb47f07427dae194a9c75baec1d81d9259"
},
"serverParameters": {
"internalQueryFacetBufferSizeBytes": NumberInt("104857600"),
"internalQueryFacetMaxOutputDocSizeBytes": NumberInt("104857600"),
"internalLookupStageIntermediateDocumentMaxSizeBytes": NumberInt("104857600"),
"internalDocumentSourceGroupMaxMemoryBytes": NumberInt("104857600"),
"internalQueryMaxBlockingSortMemoryUsageBytes": NumberInt("104857600"),
"internalQueryProhibitBlockingMergeOnMongoS": NumberInt("0"),
"internalQueryMaxAddToSetBytes": NumberInt("104857600"),
"internalDocumentSourceSetWindowFieldsMaxMemoryBytes": NumberInt("104857600")
},
"ok": 1
}
温馨提示
索引好用,但是切记复合索引是有顺序排列的,如果将查询字段顺序写错,是不会使用索引的哈!