MongDB索引的介绍及使用

作者: iszhenyu | 来源:发表于2016-10-13 18:41 被阅读187次

    索引的重要性,应该无需多说了,它可以优化我们的查询,而且在某些特定类型的查询中,索引几乎是必不可少的。这篇文章主要介绍了MongoDB中的几种常见的索引以及在使用时候的一些注意事项。这几种索引基本上涵盖了我们在平时开发的时候会遇到的大部分情况,但是,仍然需要注意的是,这篇文章没有涉及一些特殊的索引,比如TTL索引、全文本索引和地理空间索引,想要了解这几个索引的同学在这里可能得不到想要的答案。

    单一索引

    单一索引应该是MongoDB里最简单和最容易理解的索引,但由于MongoDB是一个非关系型数据库,它的索引结构跟常见的关系型数据库(例如MySQL)又有所不同。

    1、在单一字段上建立索引

    假设我们有一个名为records 的 collection ,其中存储的文档格式如下:

    {
      "_id": ObjectId("570c04a4ad233577f97dc459"),
      "score": 1034,
      "location": { state: "NY", city: "New York" }
    }
    

    我们希望在score字段上建立升序索引,可以执行如下语句

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

    其中1代表索引按升序排列各项,-1代表按降序排列。这个索引支持在score字段进行查询,例如下面的查询语句:

    db.records.find({score: 2})
    db.records.find({score: {$gt: 10}})
    

    2、在内嵌文档字段上建立索引

    同样,我们也可以在一个嵌入文档的字段上建立索引,注意这里是针对嵌入文档的某一个字段建立索引。还是考虑上面的文档,我们希望在location的state字段建立索引,可以如下操作

    db.records.createIndex({"location.state": 1})
    

    也就是,内嵌文档与字段之间用.分割就可以了。这个索引支持如下的查询:

    db.records.find({"location.state": "CA"})
    db.records.find({"location.city": "Albany", "location.state": "NY"})
    

    注意这里的第二个查询语句,location.state并不是第一位的查询条件,因为我们已经在location.state上加了索引,mongo会优先使用索引查询,所以这条语句也是有效的。

    3、在内嵌文档上建立索引

    在上面的例子中,location字段是一个内嵌文档,我们可以针对整个location字段来建立索引,这里需要注意与在内嵌文档字段上建立索引的区别

    db.records.createIndex({location: 1})
    

    考虑下面的查询语句

    db.records.find({location: {city: "New York", state: "NY"}})
    

    这条查询语句虽然可以使用这个刚刚建立的索引,但是却无法获取到正确的数据,因为在内嵌文档上执行精确匹配时,字段的顺序也必须跟内嵌文档中的字段顺序一致。

    我们上面构建索引的方式都是阻塞式的,也就是当我们在构建索引的时候,所有的数据都不可访问,直到索引构建完毕,这在正式环境中是绝对无法忍受的,解决这个问题也很简单,我们只要传递background参数就可以了,像下面这样db.people.createIndex( { zipcode: 1}, {background: true} ),这样mongo会在后台构建索引而不会阻塞其他的操作。

    复合索引

    简单来说,复合索引就是一个建立在多个字段上的索引。我们在正式开发的时候,很多查询条件都不是单一的,甚至有的时候排序方向都未必是单一的,这个时候复合索引就变得非常有用了。它的功能很强大,但同时,使用起来也很复杂。

    1、建立一个复合索引

    建立复合索引的语法是这样的

    db.collection.createIndex({<field1>: <type>, <field2>: <type>,...})
    

    其中,<type>指的就是那个字段索引的类型,比如为1说明是升序,-1说明是降序。

    考虑一个名为products的collection,其中的文档如下所示

    {
     "_id": ObjectId(...),
     "item": "Banana",
     "category": ["food", "produce", "grocery"],
     "location": "4th Street Store",
     "stock": 4,
     "type": "cases"
    }
    

    我们可以使用如下的语句在itemstock上建立了复合索引

    db.products.createIndex({"item": 1, "stock": 1})
    

    在这里,itemstock的顺序很重要,这个索引指向的文档首先会根据item字段来排序,而针对每个item字段值,会再根据stock字段来排序。

    除了支持匹配所有索引字段的查询外,复合索引还支持匹配索引字段前缀的查询,例如,在建立上面的复合索引后,下面的查询语句也是被索引支持的

    db.products.find({item: "Banana"})
    db.products.find({item: "Banana", stock: {$gt : 5}})
    

    2、索引键的方向

    对于单一字段索引来说,按升序或降序排序是无关紧要的,因为MongoDB可以在相反的方向上来反转索引,然而对于复合索引,排序的次序问题就会对索引是否支持排序操作产生影响。

    考虑一个名为events的集合,里面的每个文档都包括usernamedate字段,现在有一个查询,希望将结果按username升序,date降序排列

    db.events.find().sort({username: 1, date: -1})
    

    或者是将username降序date升序排列

    db.events.find().sort({username: -1, date: 1})
    

    因为相互翻转(在每个方向上都乘以-1)的索引是等效的,所以,为了支持上面两种查询操作,可以建立如下的索引

    db.events.createIndex({"username": 1, "date": -1})
    

    但是这个索引并不支持按username升序和date升序排列,也就是下面的查询是无法使用上面的索引的

    db.events.find().sort({username: 1, date: 1})
    

    那如果你真的是还有{username: 1, date: 1}这种排序需求,就只能再创建一个这个方向上的索引。

    只有基于多个查询条件进行排序时,索引的方向才是比较重要的,如果只是基于单一的键进行排序,MongoDB可以简单的从相反方向读取索引。

    3、索引前缀

    索引前缀就是索引字段的前几个字段,简单来说,如果有一个拥有N个键的索引,那么我们同时也得到了所有这N个键的前缀组成的索引。例如,有一个索引,它的结构如下

    {"item": 1, "location": 1, "stock": 1}
    

    那么,也就意味着下面这两个索引也是可用的:

    { item: 1 }
    { item: 1, location: 1 }
    

    因此,如果我们的查询条件中包括以下几个字段,那么MongoDB就会使用到这个索引

    1. item 字段
    2. item 和 location 字段
    3. item 和 location 和 stock 字段

    而在下面的几个字段上是无法使用这个索引的

    1. location 字段
    2. stock 字段
    3. location 和 stock 字段

    最后我们考虑另外一种情况,假设我们现在有个如下所示的查询条件

    { item: xx, stock: xx }
    

    那么这个查询会用到上面创建的索引吗?答案是肯定的,因为item字段可以看成是一个索引前缀,所以mongo首先会按item来查询,但是对于stock字段就无能为力了,因此,如果我们希望进一步加速上面的查询,则需要在itemstock两个字段上再建立索引。

    多键值索引

    多键值索引又叫数组索引,它是为了能够高效搜索数组中特定元素而创建的索引。为了数组字段上建立索引,MongoDB会为数组中的每个元素建立索引条目,这也是为什么将数组索引称为多键值索引的原因。创建数组索引对数组中的元素类型没有太多要求,无论数组中元素是标量值(eg. strings、numbers)还是内嵌文档都是可以的。

    可以使用如下的语法来创建数组索引

    db.collection.createIndex({<field>: <1 or -1>})
    

    我们发现,这与创建普通索引的语法是一样的,而不需要我们显示的指定为多键值索引,如果索引的字段是一个数组,MongoDB会自动创建为多键值索引。

    我们在第二节提到了复合索引,并且在官方文档上也有明确标明:一个索引中的数组字段最多只能有一个。例如下面的文档

    {
        _id: 1,
        a: [1, 2],
        b: [1, 2],
        category: "AB - both arrays"
    }
    

    我们是无法创建{a: 1, b: 1}这样的复合多键索引的,因为a和b字段都为数组。那么我们不禁要问,问什么不可以呢?

    我们知道,对于数组索引来说,数组中的每个元素都会被索引,假如字段a的元素个数为5,那么就要创建5条索引条目,而像上面的例子,a、b字段上每一对可能的元素都要被索引,也就是一共会有2 × 2个索引条目,当数组的长度很大时,索引的条目就会爆炸性增长,因此,为了避免这个问题,MongoDB规定只能最多有一个数组字段。

    但是考虑另外一种情况,一个集合里面包含如下的文档:

    { 
        _id: 1, 
        a: [1, 2], 
        b: 1, 
        category: "A array" 
    }
    { 
        _id: 2, 
        a: 1, 
        b: [1, 2], 
        category: "B array" 
    }
    

    一个复合多键索引{a: 1, b: 1}是允许的,因为对于每一个文档,只有一个字段为数组。当这个索引建立后,如果你尝试插入一条a和b都为数组的数据,将导致失败。

    下面通过几个例子来更好的了解数组索引。

    索引基本数组
    考虑一个集合:survey,它存储了如下形式的文档:

    {
        _id: 1,
        item: "ABC",
        ratings: [2, 5, 9]
    }
    

    我们在ratings字段建立索引

    db.survey.createIndex({ratings: 1})
    

    因为ratings字段包含数组,索引ratings上的索引为多键值索引,这个多键值索引包含如下三个索引键,每一个都指向了相同的文档:

    • 2
    • 5
    • 9

    索引元素为嵌入文档的数组

    考虑下面的文档

    {
      _id: 1,
      item: "abc",
      stock: [
        { size: "S", color: "red", quantity: 25 },
        { size: "S", color: "blue", quantity: 10 },
        { size: "M", color: "blue", quantity: 50 }
      ]
    }
    {
      _id: 2,
      item: "def",
      stock: [
        { size: "S", color: "blue", quantity: 20 },
        { size: "M", color: "blue", quantity: 5 },
        { size: "M", color: "black", quantity: 10 },
        { size: "L", color: "red", quantity: 2 }
      ]
    }
    {
      _id: 3,
      item: "ijk",
      stock: [
        { size: "M", color: "blue", quantity: 15 },
        { size: "L", color: "blue", quantity: 100 },
        { size: "L", color: "red", quantity: 25 }
      ]
    }
    
    ...
    

    下面的语句在 stock.size 和 stock.quantity 字段上建立了多键值索引

    db.inventory.createIndex( { "stock.size": 1, "stock.quantity": 1 } )
    

    如下的查询语句都是OK的

    db.inventory.find( { "stock.size": "M" } )
    db.inventory.find( { "stock.size": "S", "stock.quantity": { $gt: 20 } } )
    db.inventory.find( ).sort( { "stock.size": 1, "stock.quantity": 1 } )
    db.inventory.find( { "stock.size": "M" } ).sort( { "stock.quantity": 1 } )
    

    使用索引

    了解索引只是我们使用的第一步,我们最终的目的是能够高效的利用它们,因此,我们在创建索引的时候就要很清楚的知道,哪些查询是完全无法使用索引的,哪些查询是可以比其他查询能够更高效使用索引的。为了能够达到这一点,我们就需要了解MongoDB对各种不同查询操作符是怎么处理的。

    $操作符

    $where
    这种情况是完全无法使用索引,索引查询中尽量避免使用$where

    $exists
    无法使用索引,因为在索引中,不存在的字段和null字段的存储方式是一样的,查询必须遍历每一个文档检查这个值是否真的为null还是根本不存在。

    $ne
    可以使用索引,但是不高效,因为要查看所有的索引条目。

    $not
    有时能使用索引,但通常MongoDB并不知道如何去使用,对与基本的范围查询和正则表达式查询,MongoDB会进行反转,例如{"key": {"$lt": 7}}会变成{"key": {"$gte": 7}},但是大部分时候这种查询会退化为全表扫描。

    $nin
    这个查询和$where的行为一样,总是会进行全表扫描,因此也要极力避免使用。

    何时不使用索引

    大部分时候,索引都可以帮助我们高效的获取结果,但也有一些查询不使用索引会更快。因为使用索引需要进行两次查询,一次是查找索引条目,一次是根据索引指针去查找相应的文档,所以,如果我们要查找的结果集在原集合中所占的比例越大,索引的速度就越慢,而全表扫描只进行一次文档的查找。一般来说,如果查询需要返回集合内30%或者更多的数据,就需要对使用索引和全表扫描的速度进行比较,从而决定是否需要使用索引。


    这样学机器学习

    相关文章

      网友评论

        本文标题:MongDB索引的介绍及使用

        本文链接:https://www.haomeiwen.com/subject/zfzwyttx.html