Skip to main content

MongoDB基础知识之索引

MongoDB基础知识之索引简介

索引能够支持在MongoDB中的高效查询。如果没有索引的话,MongoDB必须执行全表扫描,例如:扫描集合中的每一个文档,以选择匹配查询条件的文档。对于一个查询而言,如果存在一个合适的索引,MongoDB就可以使用索引来控制它需要检查的文档数目。本周我们就来聊一下MongoDB索引的一些基础知识。

00:00/00:00

索引是特殊的数据结构(MongoDB索引使用一个B树数据结构),使用一种易于遍历的形式存储数据集中的一小部分数据。索引存储了一个特定字段的值或者是一系列特定字段的值,并且通过字段的值进行排序。索引条目的顺序支持高效的等量匹配和基于范围的查询操作。此外,MongoDB还可以通过使用索引中的排序来对结果进行排序。

下面的图展示了使用一个索引来对匹配文档进行选择和排序的查询:

本质上说来,MongoDB中的索引与其它数据库系统中的索引类似。MongoDB在集合级别定义了索引,支持在MongoDB集合上的文档中任何字段或者子集字段上的索引。

默认的_id索引

MongoDB在集合创建的时候就会在_id字段上创建一个唯一索引。_id索引防止客户端插入有两个相同_id字段值的文档。我们无法删除_id字段上的索引。

注意:在分片集群中,如果我们不使用_id字段作为片键,那么我们的应用必须确保_id字段值的唯一性以防止报错。在大多数情况下,一般都使用一个自动生成的 ObjectId解决这个问题。

创建一个索引

使用db.collection.createIndex()或者驱动中的相似方法创建一个索引:

db.collection.createIndex( <key and index type specification>, <options> )

db.collection.createIndex()方法只会在没有相同说明的索引存在的情况下创建一个索引。

索引类型

MongoDB提供了许多不同索引类型以支持特定类型的数据和查询。

单字段

除了MongoDB定义的_id索引,MongoDB支持在文档的单个字段上创建用户自定义的升序/降序索引。
对于单一字段的索引和排序参数,索引的排列顺序(例如:升序和降序)没有任何影响,因为MongoDB可以从两边遍历。

示例

  1. 在单一字段上创建一个索引:
    records集合是一个存储着类似于下面文档示例的集合,文档示例如下:
{
  "_id": ObjectId("570c04a4ad233577f97dc459"),
  "score": 1034,
  "location": { state: "NY", city: "New York" }
}

下面的操作在record集合的score字段上创建了一个升序索引。

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

其中,1表示升序索引,而-1则表示降序索引。创建的索引支持在score字段的查询操作,例如:

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

2.在嵌入字段上创建一个索引
和对文档的顶级字段进行索引一样,我们也可以对嵌入文档中的字段进行索引。同样地,假设record集合中的示例文档如下:

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

下面的操作在location.state字段上创建索引:

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

创建的索引可以支持在location.state字段上的查询,如下:

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

3.在嵌入文档上创建一个索引
我们也可以在整个嵌入文档上创建一个索引,同样地,record集合的示例文档如下:

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

location字段是一个嵌入文档,包含嵌入的字段statecity。下面的命令直接在location这个字段上创建了一个索引:

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

创建的索引可以支持在location字段上的查询,如下:

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

注意:尽管该查询可以使用该索引,但是结果集中不会包含上述示例。当在嵌入文档上执行等量匹配时,字段的顺序对结果也有影响,嵌入文档必须完全匹配。

复合索引

MongoDB也支持在多个字段上自定义的索引,例如:复合索引。在复合索引中列举出来的字段顺序有非常大的影响。例如,如果一个复合索引为{userid:1, score:-1},那么索引首先根据userid进行排序,然后在每个相同的userid值中,根据score进行排序。

对于复合索引和排序操作,索引的排列顺序(例如:升序或降序)会决定该索引是否能够支持一个排序操作。


示例

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

注意:不能在复合索引中包含哈希索引类型。如果我们尝试创建包含哈希索引字段的复合索引,将会出现错误。
product集合的示例文档如下:

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

下面的操作在itemstock字段上创建了升序索引:

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

复合索引中列出的字段顺序非常重要。说明将会包含首先根据item字段值进行排序的文档应用。而在item字段值相同的情况下,根据stock字段值进行排序。
除了支持匹配所有索引字段的查询,复合索引还可以支持索引字段前缀上的匹配查询。也就是说,上面的索引既支持item字段上的查询,又可以支持itemstock字段的查询:

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

上面提到,当字段索引中,键值的排序并不会影响结果,MongoDB可以从两个方向进行遍历。然而,对于复合索引,排序在确定索引是否能支持一个排序操作上会有影响。
排序规则
考虑一个包含字段usernamedate字段的集合events。应用可以发出一个查询:先根据username升序排列然后根据date值进行降序排列(例如,最新的出现在前面),例如:

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

或者根据username降序排列然后根据date升序排列,例如:

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

下面的索引可以同时支持上面的排序操作:

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

然而,上面的索引并不能支持根据usernamedate都升序排列,例如:

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

我认为:理由可能在于索引的结构,复合索引中的存储结构是先根据某个字段进行排序,再在该字段相同值的情况下根据后面的索引字段进行排序,因此索引可以直接从两边进行检索。但是如果不匹配,则需要在某个序列段内部反向排序。
前缀
索引的前缀为索引字段的前序子集。例如,考虑下面的复合索引:

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

该索引有以下索引前缀:

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

对于一个复合索引来说,MongoDB可以使用索引支持在前缀索引上的查询。因此,MongoDB可以在下列字段上使用该索引进行查询:

  • item字段
  • item字段和location字段
  • item字段、location字段和stock字段
    MongoDB也可以使用该索引来支持在item字段和stock字段上的查询。但是它将不会有直接在只包含itemstock字段上创建的索引高效。此外,MongoDB也不能使用该索引来支持包含下面字段的查询(不包含item字段):
  • location 字段
  • stock 字段
  • locationstock 字段

如果在某个集合中同时包含一个复合索引和它的前缀(例如:{ a: 1, b: 1 }{ a: 1 }),如果两个索引都没有稀疏或唯一限制,那么我们就可以删除在前缀上的索引(即{ a: 1 })。

因此,在复合索引中,各索引字段的排列顺序非常重要,建议为:查询中的等值字段、排序字段、范围查询。原因在于:等值字段可以过滤掉一部分文档,减少读取到内存中的文档数目;接着需要在范围查询字段和排序字段做个权衡,如果索引中没有排序,那么MongoDB需要在内存中再对文档排序,需要花费一点时间,因此建议把排序的字段放在范围查询字段之前。

举个小例子:常用的查询语句为:

db.test.find({type:1,id:{$gt:0,$lt:1000000}}).sort({time:-1})

那么一般情况下,比较合适的复合索引创建方式为:

db.test.ensureIndex({type:1,,time:-1,id:1},{name:'index_tist'})

多键索引

MongoDB使用多键索引来索引存储在数组中的内容。如果我们对保存着数组值的字段进行索引,那么MongoBD会为该数组的每一个元素都创建单一的索引条目。这些多键索引允许查询通过匹配数组中的元素来选择包含数组的文档。如果索引的字段包含一个数组值,MongoDB自动决定是否创建一个多键索引,我们并不需要显式指定多键类型。

如果某个索引字段是数组类型的话,那么MongoDB就会自动创建多键索引,我们并不需要显式指定多键类型:

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

注意:对于符合的多键索引,每个索引的文档中最多只能有一个索引字段值是数组。也就是说:我们不能在已经存在一个数组索引的情况下去创建一个复合的多键索引。
例如,考虑下面的文档示例:

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

我们不能在集合中指定复合多键索引{a:1,b:1},因为ab字段都是数组。
但是,考虑包含下面文档的集合:

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

可以允许复合多键索引{a:1,b:1},因为对于每个文档而言,复合多键索引中只有一个字段是数组,即:没有文档的ab的字段值都是数组。而创建了复合多键索引之后,如果我们尝试向集合中插入ab值同时是数组的文档,MongoDB将会阻止这个插入。

注意:不能指定多键索引作为片键索引,而哈希索引也不能是多键索引,一个多键索引无法支持覆盖查询。

地理空间索引

为了支持对地理空间坐标数据上的高效查询,MongoDB提供了两种特殊的索引:2d索引(在返回结果时使用平面地理距离)以及2dsphere索引(根据球面距离返回结果)。
地理索引在之前的文章中已经说过,这里就不详细介绍了。

文本索引

MongoDB提供了一个文本索引类型,能够支持对集合中字符串内容的检索。这些文本索引。这些文本索引不会存储特定语言的停用词(例如:thea以及or等),会提取结合中的词干以便于只存储词根。
注意:一个集合中最多只能有一个text索引。

如果需要在一个包含字符串或字符串数组的字段上创建索引,在索引文档中包含字段名称,并且指定text类型,如下:

db.reviews.createIndex( { comments: "text" } )

我们可以使用text索引来索引多个字段。下面的示例在subjectcomments字段上创建了一个text索引:

db.reviews.createIndex(
   {
     subject: "text",
     comments: "text"
   }
 )

指定权重
对于一个文本索引而言,索引字段的权重表示在文本检索分数中该字段与其它字段的相对重要性。对于文档中的每个索引字段,MongoDB将权重与匹配的数目相乘求和。然后MongoDB将会基于这个值计算出该文档的分数。
示例索引创建如下:

db.blog.createIndex(
   {
     content: "text",
     keywords: "text",
     about: "text"
   },
   {
     weights: {
       content: 10,
       keywords: 5
     },
     name: "TextIndex"
   }
 )

其中,content字段的权重为10,keywords字段的权重为5,about字段的权重为1。

通配符文本索引
在对多个字段创建文本索引时,我们可以可以使用通配符($**)。通过使用该符号,MongoDB对集合中每个文档中包含字符串的字段进行索引。示例如下:

db.collection.createIndex( { "$**": "text" } )

该索引允许在所有字符串内容的字段上进行文本索引。这个索引在高度非结构化的数据中非常有用,特别是在不清楚在文本索引中包含什么字段的时候。
(PS:企业版中提供了对中文文本检索的支持。)

哈希索引

为了支持基于哈希的分片,MongoDB提供了一个哈希索引类型,对某个字段值的哈希值进行索引。这些索引在范围上有一个更加随机的分布,但是支持等量匹配。并不能支持基于范围的查询。
创建方式如下:

db.collection.createIndex( { _id: "hashed" } )

索引性质

唯一索引

索引的唯一性会造成MongoDB不接受索引字段上的重复值。除了唯一性约束。唯一索引在功能上可以与其它MongoDB索引一同使用。

部分索引

3.2版本中新增的功能。部分索引只对集合中满足特定过滤条件的文档进行索引。通过索引集合中的文档子集,部分索引在索引创建和维护阶段实现更少的存储需求以及更低的性能支出。
部分索引提供了稀疏索引功能的超集,与稀疏索引相比,应该优先考虑部分索引。

稀疏索引

索引的稀疏属性能够保证索引只包含那些文档中拥有索引字段的条目。稀疏索引会跳过那些不包含索引字段的文档。
我们可以结合使用稀疏索引选项和唯一索引选项以拒绝那些在某个字段上有重复值的文档,但是忽略那些没有改索引键的文档。

TTL索引

TTL索引是一个特殊的索引,MongoDB可以使用该索引在一定时间之后自动删除集合中的文档。这对于一些类型的数据而言是非常理想的,例如:机器生成的事件数据、日志以及只需要存储在一个数据库中一段确定时间的会话信息等。

在一定时间之后删除文档
创建的代码如下所示:

db.log_events.createIndex( { "createdAt": 1 }, { expireAfterSeconds: 3600 } )

该操作在log_events数据集的createAt字段上创建索引并且指定expireAfterSeconds的值为3600,将过期时间设置为createAt指定的时间之后的1个小时。
在向log_events集合中增加文档时,将createAt字段的值设置为当前时间:

db.log_events.insert( {
   "createdAt": new Date(),
   "logEvent": 2,
   "logMessage": "Success!"
} )

当文档的createAt字段值老于expireAfterSeconds参数值指定的秒数之后,MongoDB将会自动从log_events集合中删除文档。

在特定的时间删除文档
如果想要是的文档在特定的时间过期,可以在某个存储着BSON日期类型的字段或者BSON数据类型对象的数组中创建TTL师徒吧,并且将expireAfterSeconds的值设置为0。对于集合中的每一个文档,将索引的日期字段设置为希望文档过去的时间。如果索引的日期字段包含过去日期的值,那么MongoDB将会将该文档看做过期的值。

例如,下面的操作在log_events集合的expireAt字段上创建了一个索引,并且将expireAfterSeconds的值设置为0

db.log_events.createIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } )

对于每个文档,将expireAt的值设置为文档应该过期的时间。例如,下面的insert操作增加了一个应该在2013年7月22日 14:00过期的文档:

db.log_events.insert( {
   "expireAt": new Date('July 22, 2013 14:00:00'),
   "logEvent": 2,
   "logMessage": "Success!"
} )

MongoDB将会在文档的expireAt值老于expireAfterSeconds参数值指定的秒数之后(在本示例中为0),自动从log_events集合中删除文档。

索引和排序规则

为了在字符串比较中使用索引,该操作也必须指定相同的排序规则。也就是说,一个包含着排序规则的索引不能支持使用不同排序规则在索引字段上进行字符串比较的操作。
例如,集合myColl在字符串字段category上有一个排序本地环境fr

db.myColl.createIndex( { category: 1 }, { collation: { locale: "fr" } } )

下面的查询操作,使用相同的排序规则作为索引,可以使用该索引:

db.myColl.find( { category: "cafe" } ).collation( { locale: "fr" } )

然而,下面的查询操作,默认使用simple二进制排序器,就不能使用该索引:

db.myColl.find( { category: "cafe" } )

对于一个索引前缀键值不是字符串、数组以及嵌入文档的复合索引而言,一个指定不同排序规则的操作仍然可以使用索引来支持索引前缀键值的比较。
例如,集合myColl在数值字段scoreprice和字符串字段category上有复合索引,而在创建索引时则指定了排序规则本地环境fr用作字符串比较:

db.myColl.createIndex(
   { score: 1, price: 1, category: 1 },
   { collation: { locale: "fr" } } )

下面的操作,使用simple二进制排序规则用于字符串比较,也可以使用索引:

db.myColl.find( { score: 5 } ).sort( { price: 1 } )
db.myColl.find( { score: 5, price: { $gt: NumberDecimal( "10" ) } } ).sort( { price: 1 } )

下面的操作,在索引的category字段上使用simple二进制排序规则用户字符串比较,则只可以使用索引来完成这个查询中score:5这个部分的的查询:

db.myColl.find( { score: 5, category: "cafe" } )

覆盖查询

当查询条件和查询映射包括索引字段时,MongoDB将会直接从索引中返回结果,而不需要扫描任何文档或者将文档放入内存中。这些被覆盖的查询非常高效。

索引交集

从2.6开始,MongoDB就可以使用索引交集来进行查询、对于制定了复合查询条件的查询,如果一个索引可以满足查询条件的某个部分,另一个索引可以满足查询条件的其它部分,那么MongoDB就可以使用两个索引交集来实现查询、使用符合索引更高效还是使用索引交集更高效由特定的查询和系统决定。

一些限制条件

  • 索引键限制:一个索引条目的总大小必须低于1024字节(包括基于BSON类型的结构化存储占用)
  • 每个集合的索引数目限制:单个集合上的索引数不能超过64个
  • 索引名称长度:合格的完整索引名称(包括命名空间和点操作符,例如<database name>.<collection name>.$<index name>),长度不能超过128个字符。默认地,索引名称是字段名称和索引类型的结合。我们可以在createIndex()方法中指定索引名称来保证完整的索引名称不超过限制
  • 一个复合索引中索引字段的数目:一个复合索引中包含的字段不能超过31个
  • 查询中不能同时使用文本和地理空间索引:我们不能将一个需要特殊文本索引的$text查询与一个需要不同类型的特殊索引的操作符结合使用。例如,我们不能同时使用$text查询和$near操作符。
  • 2dsphere索引的字段只能存储地理信息:2dsphere索引的字段必须存储坐标对或者GeoJSON格式的地理说句。如果我们尝试插入一个2dsphere索引的字段为非地理信息的数据,或者在一个字段中有非地理信息数据的集合中创建一个2dsphere索引,该操作会失败
  • WiredTiger存储引擎中,从被覆盖的查询中返回的NaN值通常是double数据类型:如果被某个索引覆盖的查询返回的字段值为NaN,那么NaN值的类型通常是double
  • 多键索引:多键索引不能支持覆盖查询

其它考量

虽然索引可以提高查询性能。索引也需要一些运维方面的考量。如果我们的集合存储着大量的数据,而我们的应用需要在索引创建的过程中能够存取数据,那么我们就可以考虑在后台创建索引,示例如下:

db.people.createIndex( { zipcode: 1}, {background: true, sparse: true } )
打赏
微信扫一扫支付
微信logo微信扫一扫, 打赏作者吧~

mickey

记录生活,写给几十年后的自己。