mongo索引
Posted 乾复道
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了mongo索引相关的知识,希望对你有一定的参考价值。
前言
在查询操作中,如果没有索引,[MongoDB](http://c.biancheng.net/mongodb/) 会扫描集合中的每个文档,以选择与查询语句匹配的文档。如果查询条件带有索引,MongoDB 将扫描索引, 通过索引确定要查询的部分文档,而非直接对全部文档进行扫描
索引可以提升文档的查询速度,但建立索引的过程需要使用计算与存储资源,在已经建立索引的前提下,插入新的文档会引起索引顺序的重排。
MongoDB 的索引是基于 B-tree [数据结构](http://c.biancheng.net/data_structure/)(mysql是B+Tree)及对应算法形成的。树索引存储特定字段或字段集的值,按字段值排序。索引条目的排序支持有效的等式匹配和基于范围的查询操作。
下图所示的过程说明了使用索引选择和排序匹配文档的查询过程。
MongoDB 在创建集合时,会默认在 _id 字段上创建唯一索引。该索引可防止客户端插入具有相同字段的两个文档,_id 字段上的索引不能被删除。
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.无法使用索引的操作
-
w
h
e
r
e
和
where和
where和exists无法使用索引,
$exists
操作, 会遍历每个文档,以确定字段是否存在,$where
会同样的遍历每个文档。 - 类似MySQL, 在mongo中使用取反的查询操作, 也会导致索引利用效率低下, 或者不会使用索引, 例如
$ne
, 而$not
虽然有时能够使用索引, 但是通常它并不知道该如何利用索引。 $nin
, 总是扫描整个集合。
4. 索引类型
MongoDB 中索引的类型大致包含单键索引、复合索引、多键值索引、地理索引、全文索引、 散列索引等,下面简单介绍各类索引的用法。
其中地理索引、全文索引、 散列索引,使用场景特殊,如需使用请自行查找相关文档
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"
发现使用的是BasicCursor,那么就代表我们没有索引,当我们查某一个数据的时候,就是从头到尾的扫一遍
- 索引提高查询速度,但是会降低写入和更改速度,权衡常用的查询字段,不必在太多列上建索引
a) 新增一条数据的同时,还会新增索引文件,所以会降低写入和更改速度,所以需要权衡字段,没必要添加太多的索引
2.在mongodb中,索引可以按字段升序(1)/降序(-1)来创建,便于排序
-1:磁盘上,二叉树倒序排序,由大到小,我们人眼是看不出来的
3.默认是用btree来组织所以文件,2.4版本以后,也允许建立hash索引
索引的分类:
常用命令:
- 查看当前索引状态:db.collection.getIndexes();
- 创建普通的单列索引:db.collection.ensureIndex(field:1/-1);//1代表升序,-1代表降序
- 创建多列索引:db.collection.ensureIndex(field:1/-1,field2:1/-1);
我们如何优化mysql的时候,有多人说,为where语句常用的字段添加索引,但是仔细想想,这么说实际上是错误的,因为我们的where语句,经常会好几个字段一起用,但是只能有一个索引起作用。所以应该建立多列索引。在mongo中也是这样的
多列索引和为每一个列都建立索引是不一样的,多列索引是把多个列看成一个整体,进行索引。如果两个列经常放在一起查,那么,使用多列索引要比在每一个列上加索引效率要高。
4.创建子文档索引:db.collection.ensureIndex(field.subfield:1/-1);
我们插入的数据为:
db.shop.insert({name:’Nokia’,spc:{weight:120,area:’taiwan’}});
{weight:120,area:’taiwan’}是子文档,如何在子文档中添加索引?使用点’.’,如下图
5.创建唯一索引:db.collection.ensureIndex({field.subfield:1/-1},{unique:true});
注意:唯一索引,这个列上的值不能重复,下图中,我们在添加了唯一索引的列email中,添加两条相同的值,会报error
6.创建稀疏索引:
稀疏索引的特点----如果针对field做索引,针对不含field列的文档,将不建立索引,与之相对,普通索引,会把该文档的field列的值认为NULL,并建立索引
适宜于:小部分文档含有某列时
db.collection.ensureIndex({field:1/-1},{sparse:true});
创建非稀疏索引:结果如下:
删除索引之后,再添加稀疏索引,结果如下
7.创建哈希索引(2.4新增的)
哈希索引速度比普通索引要快,但是无法对范围查询进行优化,适宜于—随机性强的散列
简要介绍一下哈希索引:哈希索引是通过把key给哈希函数,然后通过哈希函数计算出一个唯一值,从而决定了这个数据存放在磁盘的什么位置上。所以值的存储是散列的,没有可循的规律性,当我们要查找一个数据的时候,比如查找学号为99的学生,我们可以这样。但是我们要查找200-500这样一个范围内的所有学生,哈希索引显然就不合适了。因为它的存储不是连续的,在这种情况普通索引就要好很多。
db.collection.ensureIndex({field:’hashed’});
8.删除单个索引
db.collection.dropIndex({field:1/-1});//字段,和排序方式(1/-1)都需要指明
删除所有索引
db.collection.dropIndexes();
9.重建索引
_id索引是自带的,必须会有的
email_1 升序排列
以上是关于mongo索引的主要内容,如果未能解决你的问题,请参考以下文章