前言

    在查询操作中,如果没有索引,[MongoDB](http://c.biancheng.net/mongodb/) 会扫描集合中的每个文档,以选择与查询语句匹配的文档。如果查询条件带有索引,MongoDB 将扫描索引, 通过索引确定要查询的部分文档,而非直接对全部文档进行扫描

     索引可以提升文档的查询速度,但建立索引的过程需要使用计算与存储资源,在已经建立索引的前提下,插入新的文档会引起索引顺序的重排。

    MongoDB 的索引是基于 B-tree [数据结构](http://c.biancheng.net/data_structure/)(MySQL是B+Tree)及对应算法形成的。树索引存储特定字段或字段集的值,按字段值排序。索引条目的排序支持有效的等式匹配和基于范围的查询操作。

    下图所示的过程说明了使用索引选择和排序匹配文档的查询过程。

    MongoDB 在创建集合时,会默认在 _id 字段上创建唯一索引。该索引可防止客户端插入具有相同字段的两个文档,_id 字段上的索引不能被删除。

image.png

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})

image.png

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"),
            "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
}


温馨提示

索引好用,但是切记复合索引是有顺序排列的,如果将查询字段顺序写错,是不会使用索引的哈!

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐