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和 whereexists无法使用索引,$exists操作, 会遍历每个文档,以确定字段是否存在, $where会同样的遍历每个文档。
  • 类似MySQL, 在mongo中使用取反的查询操作, 也会导致索引利用效率低下, 或者不会使用索引, 例如$ne, 而$not虽然有时能够使用索引, 但是通常它并不知道该如何利用索引。
  • $nin, 总是扫描整个集合。

4. 索引类型

    MongoDB 中索引的类型大致包含单键索引、复合索引、多键值索引、地理索引、全文索引、 散列索引等,下面简单介绍各类索引的用法。

其中地理索引、全文索引、 散列索引,使用场景特殊,如需使用请自行查找相关文档

4.1 单键索引

单字段索引和排序操作,索引键的排序顺序(即升序或降序)无关紧要,因为 MongoDB 可以在任意方向上遍历索引。

  • value: 1 为升序,-1 为降序

db.collection.createIndex(filedName: value)

4.2 复合索引

就是在创建索引的时候同时指定多个字段

注意事项

  1. 索引是区分方向的
    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也是一致的。
  2. 索引字段要有序
    为了能够更加有效的利用索引, 用于精确匹配的字段应该放在索引的前面, 范围字段放在后面, 这样mongo就能利用精确匹配过滤掉大部分文档, 然后利用之后的索引去再次过滤结果。例如我们要查询name=user10&age>20, 那么我们的索引应该是"name": 1, "age": 1, 而不是"age": 1, "name": 1
  3. 隐式索引
    如果我们创建一个复合索引"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字段含义

字段含义
stageCOLLSCAN全表扫描
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,那么就代表我们没有索引,当我们查某一个数据的时候,就是从头到尾的扫一遍

  1. 索引提高查询速度,但是会降低写入和更改速度,权衡常用的查询字段,不必在太多列上建索引

a)      新增一条数据的同时,还会新增索引文件,所以会降低写入和更改速度,所以需要权衡字段,没必要添加太多的索引

  2.在mongodb中,索引可以按字段升序(1)/降序(-1)来创建,便于排序

-1:磁盘上,二叉树倒序排序,由大到小,我们人眼是看不出来的

  3.默认是用btree来组织所以文件,2.4版本以后,也允许建立hash索引

索引的分类:

常用命令:

  1. 查看当前索引状态:db.collection.getIndexes();
  2. 创建普通的单列索引:db.collection.ensureIndex(field:1/-1);//1代表升序,-1代表降序
  3. 创建多列索引: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索引的主要内容,如果未能解决你的问题,请参考以下文章

MongoDB

MongoDB介绍以及安装

MongoDB 安装与启动

NoSQL(Mongo)的面向文档的数据抽象层?

mongodb快速入门

mongoDB的基本使用----飞天博客