美文网首页我爱编程
MongoDB(index)

MongoDB(index)

作者: 我看不见 | 来源:发表于2016-01-26 20:14 被阅读1524次

    索引(index)

    索引 index经常用于常用的查询,如果设计得好,在创建索引之后的查询会有提升效率的效果。但是用之不当的话也可能会没有任何效果,甚至产生反效果,还浪费空间去存储索引信息。因为它事关数据的存储方式,和storage engine相关。
    如下是一个使用了index的例子,其中只是创建了一个score的索引,此时如果我们需要查询的field与score有关的话,查询起来可能就会变得快了。注意只是“可能”,这与索引的设置方式有关。“快”是因为引擎中存储了index与对应的文档索引,类似于“array[index]=文档下标”,所以可以通过index直接找到那些在collection中的文档。一般情况下是逐个取出,即在index中取出一个“下标”,然后在存储collection区域索引这个“下标”并取出此文档,因此要考虑效率相关的问题。

    index-for-sort.png

    支持性

    官方文档写道“ MongoDB defines indexes at the collection level and supports indexes on any field or sub-field of the documents in a MongoDB collection. ”关键词是“collection level”和“any field or sub-field”。实现index的数据结构是B-tree。

    单索引(single field)

    MongoDB支持对任何field的索引,而且默认存在的索引是升序的_id域。此外,MongoDB支持创建除了_id域之外的单个field的文档索引,可以指定为升/降序。如果只是对一个field创建索引,那么升降序都是无所谓的,因为可以从任一边进行遍历index。

    多索引(compound index)。

    多个field组合的索引也是可以创建的,但是创建索引时所指定field的顺序是很重要的,直接关系到在排序时这个索引是否能提供便捷。比如创建的索引是{ userid: 1, score: -1},意味着索引是根据userid排序的,对于userid相同的才根据score进行内部排序。此时如果你想要排序的规则是{score: 1, userid: 1},那么MongoDB很可能做不到任何优化。因为它只是提供了{ userid: 1, score: -1}{ userid: -1, score: 1}这两种排序的支持。
    关于前缀,比如有{ "item": 1, "location": 1, "stock": 1 }这样的一个index,它的前缀有 { item: 1 }{ item: 1, location: 1 }{ "item": 1, "location": 1, "stock": 1 },这些前缀也支持便捷查询提供便捷。其他的field就不支持了。如果想使用{ "item": 1, "stock": 1 }来排序的话,也是可以的,但是总会比单个item或stock要慢。如果同时有{ a: 1, b: 1 }{ a: 1 }两个index存在,其实{ a: 1 }是多余的,因为已经包含在了{ a: 1, b: 1 }里面了。

    Dot Notation

    在创建索引的时候还有一个“点”的概念,作用是可以建立内嵌文档的索引,这样的索引可以让你根据内嵌文档的相关属性来查找整个collection。“点”的格式就像是scores.score。比如有如下文档:

    {
      name: "Tom",
      age: 20,
      scores: {
        {  score: 99,
           class: "history"
        },
        {  ...
        }
        ...
      }
    }
    

    然后执行db.students.createIndex({'scores.score':1});就可以成功创建基于内嵌文档属性的索引了。

    内嵌field的index(multikey)

    如果文档中含有array,可以直接对其名称建立索引,这样MongoDB就会为内嵌数组中的每个元素建立一个独立的索引。比如有内嵌数组arr:[10086,10010],那么创建索引是db.collection.createIndex({"collection.arr": 1})
    但是有些索引是不允许创建的。比如一个文档中含有a和b两个array,你可能会这样创建索引{ a: 1, b: 1 } ,不幸的是,这样是不允许的。可能是因为a*b之后所创建的索引可能太大了。如果{ a: 1, b: 1 }的索引已经创建了,则a和b当中必定有一个是非array,此时插入一个a和b都是array的文档就会失败。
    类似的,如下的内嵌文档也可以建立索引。比如可以db.test.createIndex( { "stock.size": 1, "stock.quantity": 1 } )

    { 
      _id: 3,
      stock: [ 
        { size: "S", color: "red",  quantity: 25 },
        { size: "S", color: "blue", quantity: 10 }, 
        { size: "M", color: "blue", quantity: 50 } 
      ]
    }
    

    建立之后就可以利用于find或者sort了,比如:

    db.test.find( ).sort( { "stock.size": 1, "stock.quantity": 1 } )
    db.test.find( { "stock.size": "M" } ).sort( { "stock.quantity": 1 } )
    

    有个不同的例子,可以观察一下区别。如下的文档:

    { 
      _id: ObjectId(...), 
      metro: { 
        city: "New York", 
        state: "NY" 
      }, 
      name: "Giant Factory"
    }
    

    有两种创建索引的情况需要讨论:
    1)直接对metro创建索引db.test.createIndex( { "metro": 1 } ),那么你可以这样正常使用db.test.find( { metro: { city: "New York", state: "NY" } } )来得到index的支持,而db.test.find( { metro: { state: "NY", city: "New York" } } )就不能使用到所创建的index了。其实这样创建的index只有完全匹配了整个内嵌文档时才能发挥作用,这并没有充分发挥index特性。
    2)对metro的某个field创建索引,比如执行了db.test.createIndex( { "metro.city": 1 } ),那么使用此index应该是这样的db.test.find( { "metro.city": 1} ),而如果指定了value,比如db.users.find( { "user.login": "tester" } ),这样就不行了。

    文本索引(text index)

    当前默认版本是version 3。
    文本索引,顾名思义就是用于搜索文本的,可以用于搜索所有的value,也可以搜索指定的field对应的value。只要field对应value是string,或者对应的value是array且array中的元素是string,那么文本索引都可以索引该field。

    • 创建test index大概是这样的:
      db.reviews.createIndex( { comments: "text" } )
      或者创建复合索引是这样的:
      db.reviews.createIndex( { subject: "text", comments: "text" } )
      或者对所有"field: string"创建索引(Wildcard text index):
      db.collection.createIndex( { "$**": "text" } )

    • 删除索引仅需要指出该索引名称即可(不能删除_id索引):
      db.pets.dropIndex( "catIdx" )
      或者
      db.pets.dropIndex( { "cat" : -1 } )

    • 如果连索引名称都忘了,那么可以查询该collection设置过的所有索引:
      db.collection.getIndexes()

    更高级的操作是,可以指定权值weight,若不指定则默认每个field的weight为1。为每个需要关注的field指定一个合适的weight可以达到这样的效果,对于搜索到的每个文本串,MongoDB都计算出该文档所具有的总权值sum。sum值可以用于控制搜索的结果,具体参考$meta操作符。

    默认情况下,text index的名称为所有的field名称用_text连起来,比如db.collection.createIndex( { content: "text", "users.comments": "text", "users.profiles": "text" })的索引名称为content_text_users.comments_text_users.profiles_text。但是当名称太长了的时候,可以这样自定义索引名称db.collection.createIndex( { content: "text", "users.comments": "text", "users.profiles": "text" }, { name: "MyTextIndex" })

    哈希索引(hashed index)

    好像是用于sharding分片架构的,先mark一下。hashed index可以用来支持匹配查询,但不支持范围的查询,也不支持符合索引。

    局限性

    使用索引的同时还要注意一些限制,比如索引键的长度,一个集合可以建立多少个索引等等,先讨论一些比较重要的局限。

    关于text index,可以搭配普通的index使用,但在使用上还有一些限制,就是只能用来缩小搜索text的范围,也就要求了前面是完全匹配的索引,比如db.inventory.createIndex( { department: 1, description: "text" }),在find中使用的时候就可以这样使用了db.inventory.find( { department: "kitchen", $text: { $search: "green" } } ),效果就是在指定的department中搜索text。一般情况下,假设a是一个复合索引,那么可以这样创建索引db.collection.createIndex( { a: 1, "$**": "text" } ),此时a必须进行完全匹配再进行文本搜索才会被支持。而且,不支持与multi-key或geospatial域搭配。

    下面有一些不引人注意的限制:

    • 索引键数量的限制,当创建的索引键超过了这个限制的话,MongoDB不会再创建索引键。
    • 每个collection至多可以创建64个index。
    • 整个索引串<databasename>.<collection name>.$<index name>的长度不得超过128个字符,系统创建的index name一般是由field和 name和index type组成,使用组合index的时候就会比较长了,但index name是可以通过createIndex()方法来指定的。
    • 组合index的个数不能超过31个。
    • 一个集合至多拥有一个text index。
    • 包含搜索text的查询时不能使用hint(),更详细的参考Text Index Restrictions

    索引属性(index property)

    TTL index

    TTL index是一种作用在单个field上的索引,称为索引似乎有点误导人。其他它的作用就是设置文档的存活时长,经过了指定的秒数之后就会自动删除文档。但是这只能针对field类型是date或者是个包含date的数组(按照其中最小的一个来作为基数),其他类型则不会有自动删除的效果。
    指定时长的单位是秒,但是MongoDB会在每60秒才执行一次remove操作,所以可能会有这样的情况,你指定了10秒删一次,但是30秒了文档却仍存在,后来又不见了,就是这个原因。remove操作是在后台自动进行的,不会进行任何的提示,也不会报任何执行结果,但可以参考db.currentOp()或database profiler。如果是在replica sets的话,只会删除primary中的文档。
    一般是这样创建的TTL index:
    db.eventlog.createIndex( { "lastDate": 1 }, { expireAfterSeconds: 3600 } )
    注意,不能为_id域和复合索引指定TTL index,同时,MongoDB也不支持将TTL index作用于固定集合(capped collection)。一旦指定了TTL就不能通过createIndex()来修改时长了,也不能为同一个field指定多次TTL,只能是删除后重建。

    unique index

    只要指定了某个field是唯一的,那么在同一个collection中就不允许存在相同的field值,MongoDB默认创建的unique field就是_id。
    unique index一般是这样创建的:
    db.members.createIndex( { "user_id": 1 }, { unique: true } )
    但是这个唯一性只是在同一集合中的不同文档间有效,也就是说下面的例子并不冲突:

    db.collection.createIndex( { "a.b": 1 }, { unique: true } )
    db.collection.insert( { a: [ { b: 5 }, { b: 5 } ] } )  //此新文档中的a.b在其他文档不具备即可
    

    如果文档中没有存在unique index field,那么该文档的对应field就为null,这样的文档是可以存在的,但是默认情况下不能够有多个存在,这样就会有多个null,即冲突了(对于复合索引来说,只要组合起来是唯一就不会有冲突。)。这可以通过使用sparse index来解决这个问题。
    不能对于已经建立hashed index的field建立unique index。

    Partial Indexes(3.2 新增的)

    Partial index提供了只索引集合中的部分文档的功能,而不是全部文档。这样做的好处就是,只索引那些我们所关心的文档,比如满足某个条件的文档。在查询中使用的时候就会有些限制了,超出关心文档的范围就不能够利用到这个partial索引。
    如下是创建一个包含partial index的复合索引的例子:
    db.restaurants.createIndex( { cuisine: 1 }, { partialFilterExpression: { rating: { $gt: 5 } } })
    如果查询的范围是在关心的范围之内,那么这个partial index就起作用了,比如:
    db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } )
    然而,下面的2个例子就使用不到这个partial index了,原因是超出了关心范围 :
    db.restaurants.find( { cuisine: "Italian", rating: { $lt: 8 } } )
    db.restaurants.find( { cuisine: "Italian" } )

    注意:

    • 创建索引时不能同时指定 partialFilterExpression和sparse选项。
    • 不能创建多个仅仅是过滤表达式不同的多个版本的索引。
    • _id索引和shard key都不能是partial index。
    • 前缀的限制,要理解清楚再使用。

    Sparse Indexes

    稀疏索引只包含那些具有该field的文档(即使是null),其他的文档都会被忽略。如果我们只关心那些具有该field的文档,而这些文档又偏少,那么这样的索引就可以有效率的提升。因为普通的索引对于缺失index field的文档都是默认保存着一个null值。2dsphere (version 2)、2dgeoHaystack、text等索引永远都是sparse index。
    可以认为,部分索引partial index是稀疏索引sparse index的超集,即可以用稀疏索引实现的操作都能用是部分索引来实现,官网有清晰的例子。
    一般情况下可以这样创建一个sparse index:
    db.addresses.createIndex( { "xmpp_id": 1 }, { sparse: true } )
    配合hint()来使用这个索引在某种情况下就可以达到提升效率的效果。但是需要注意的是,这种index并不能用于sort功能(有些缺失field的文档使其无法工作)。


    创建索引

    默认情况下,在创建索引的过程中,正在执行操作的集合不允许被读写,直到操作完成。如果创建的过程比较漫长的话,你又想操作这个集合,那么可以选择在后台执行(不是真的在后台运行,在mongo shell中一旦执行创建索引操作就会被阻塞直到完成,需要另开终端才行),此时可以操作这个集合了。创建这样的索引:
    db.people.createIndex( { "name": 1}, {background: true} )
    这个选项是可以和其他选项搭配的,比如:
    db.people.createIndex( { zipcode: 1}, {background: true, sparse: true } )
    在2.4版本之前只能够有一个创建索引的操作在后台运行,现在可以同时运行多个了,但是好像只会有一个是在运作的,而其他都是处于等待队列中。而且后台运行会比前台运行要慢。如果在执行操作的过程中,mongod关闭了,那么在重启mongod之后会在前台重新开始被中断的操作。
    只要index在build的过程中遭遇任何的错误,比如重复key错误,则mongod就会出错而退出。如果出错了之后要重启,可以使用storage.indexBuildRetry 或者 --noIndexBuildRetry来跳过重新开始中断的创建过程。
    一般情况下,普通的索引名称的构造规则是这样的:
    db.products.createIndex( { item: 1, quantity: -1 } )
    索引的默认名称为:item_1_quantity_-1。
    可以在创建为其指定一个名称:
    db.products.createIndex( { item: 1, quantity: -1 } , { name: "inventory" } )


    交叉索引(index intersection)

    如果想知道find的过程中是否使用了我们创建过的索引,可以使用.explain(),比如下面的例子:
    db.orders.find( { item: "abc123", qty: { $gt: 15 } } ).explain()
    一般情况下,MongoDB会自动选择合适的索引来支持查询操作(比如匹配前缀,交换查询表达式的顺序),每次都会选择最佳的计划来执行,下次再执行就会按照最佳的方式了,还会不断更新最优计划,一切都很智能。
    只要满足如下条件之一就会重新优化最佳计划:
    1)一个集合接收到1千次的写操作。
    2)使用了reIndex操作。
    3)添加/删除一个index。
    4)重新启动mongod。

    但是,这么智能的东西也可能会达不到我们的特殊要求,此时可以用.hint()来让它按照我们的指示使用某个已存在的索引,这在充分了解其中的机理和利弊时使用可以达到特殊的目的。
    看下面的例子就知道了,比如有如下的index:
    { status: 1, ord_date: -1 }
    那么如下的查询就支持了:
    db.orders.find( { ord_date: { $gt: new Date("2014-02-01") }, status: {$in:[ "P", "A" ] } })
    其实这两个条件完全是独立的,交换顺序的结果仍是一样的,只是如果按照上面的索引,查询的时候就按索引中每个条件的顺序来查询了。复合索引中的index顺序也是很重要的,关系到查询时可以缩小的范围大小。
    但是上面的索引就不支持如下这两个操作:

    db.orders.find( { ord_date: { $gt: new Date("2014-02-01") } } )
    db.orders.find( { } ).sort( { ord_date: 1 } )
    

    因为使用index的前提是,符合某个前缀,或者顺序无关时可以使用。可以使用.explain("executionStats")来查看查询的执行信息。测试了类似于上面的例子,如果查询支持索引,那么检索的文档数大大减少,甚至等于选中的文档数。反之,如果不支持,就会将整个集合都检索一遍,这不是我们想看到的。
    使用.explain()可以大概关注几个点,比如:

    "nReturned" : <int>  选中的文档数量
    "executionTimeMillis" : <int>  本次检索所用的时间
    "totalKeysExamined" : <int>    检索了多少个索引项
    "totalDocsExamined" : <int>  检索了多少个文档
    

    其他的项如果有兴趣可以参考Explain Result


    相关文章

      网友评论

        本文标题:MongoDB(index)

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