MongoDB 基础系列十八:索引之综述

前言

此篇博文是 Mongdb 基础系列之一;

本文为作者的原创作品,转载需注明出处;

简介

用下面的一张图来描绘 MongoDB 索引的基本原理,

如图,下方的白色方格是 users 信息,蓝色方框记录的是 index 的记录;可以看到 MongoDB 默认将数据零散的存储的,然后用”轻量的” index 数据来维护数据的顺序;

索引创建

使用 db.collection.createIndex() 方法来创建索引,格式如下,

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

上述方法只会当该索引不存在的情况下,才会创建;也就是不允许对同一个索引进行重复创建;

基本索引

_id 字段索引

默认情况下,MongoDB 在创建一个 collection 的时候,会创建一个 _id 字段,该字段是一个唯一索引,保证不会重复插入两份相同的 documents;同时,该字段是不允许被删除的;

单字段索引

如图,蓝色方格既表示对 Index 创建的一个升序的索引;

sort order

任何方向上都不受限制;

在文档字段上创建索引

有如下的 records 数据,

1
2
3
4
5
{
"_id": ObjectId("570c04a4ad233577f97dc459"),
"score": 1034,
"location": { state: "NY", city: "New York" }
}

下面的操作对 records 的单字段 score 上面创建单字段索引,

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

1,表示是升序;-1,表示是降序;

下面的两个查询都会使用到该索引,

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

在内嵌文档中的单个字段上创建索引

有如下的 records 数据,

1
2
3
4
5
{
"_id": ObjectId("570c04a4ad233577f97dc459"),
"score": 1034,
"location": { state: "NY", city: "New York" }
}

下面的操作将会在内嵌文档中的单个字段 state 上创建索引,

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

下面的查询都可以使用到该索引,

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

在整个内嵌文档上创建索引

除了在内嵌文档中的单个字段上创建索引以外,我们还可以在整个内嵌文档上创建索引;假设我们有如下 records 数据,

1
2
3
4
5
{
"_id": ObjectId("570c04a4ad233577f97dc459"),
"score": 1034,
"location": { state: "NY", city: "New York" }
}

下面的操作将会在整个内嵌文档 location 上创建索引,

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

下面这个查询将会使用到上述索引,

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

Note:虽然上述的查询会使用到索引,但是并不会匹配上述的样例中的内嵌文档;内嵌文档中的字段顺序会影响到内嵌文档的匹配;

多字段复合索引( Compound Index )

上面这张图介绍了什么是多字段复合索引,“aa1”,“ca2”,”nb1”… 表示不同的 user,下面红色数字表示的是 score;

那怎么来看这张图呢?首先,排序的规则是,先根据 userid 进行升序的排列;然后,再针对相同的 userid 的不同 score 进行降序排列,该特性在 “ca2” 的排序过程中非常清楚的展示了出来;

创建一个 Compound Index

创建格式如下,

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

type 取值 1 或者 -1;1 表示升序;

假设我们有如下的 products 数据,

1
2
3
4
5
6
7
8
{
"_id": ObjectId(...),
"item": "Banana",
"category": ["food", "produce", "grocery"],
"location": "4th Street Store",
"stock": 4,
"type": "cases"
}

在字段 item 和 stock 同时创建一个升序的多字段复合索引;

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

注意,创建复合索引的时候,字段的顺序非常的重要,正如上述所创建的那样,item 在前,stock 在后,这意味着,stock 的排序是基于 item 的排序的结果之上的,当 item 的排序结束以后,再在相同的 item 元素上对 stock 元素进行升序排列;所以,stock 的排序是基于 item 的排序结果之上的;

查询除了可以对整个索引字段进行匹配以外,复合索引支持对索引字段的”前缀字段”进行匹配;比如,

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

如上所述,item 是该复合索引的“前缀”,所以,上述的两次查询都会命中 index;

Sort Order

对于复合索引,是升序还是降序会影响到该索引是否会被命中;

假设我们有如下的一个复合索引,

1
db.events.createIndex( { "userid" : 1, "score" : -1 } )

下面两种查询都会命中该 index,

  1. 方式一

    1
    db.events.find().sort( { userid: 1, score: -1 } )
  2. 方式二

    1
    db.events.find().sort( { userid: -1, score: 1 } )

为什么上述两种方式是可以命中该 index 呢?其实原理也非常的简单,看下上述的图解;第一种方式,不用说,正好“从左至右”依次匹配;第二种方式,其实正好相反,“从右至左”依次匹配,一个细节,因为 userid 是从右至左依次匹配,所以匹配 score 的时候,正好使用的是“升序”的方式,比如当遍历到 “ca2” 的时候,那么在匹配 score 的时候,就是从右至左的方式依次遍历的了;

而下面这种方式就不会命中该 index 了;

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

即便是 username 单字段排序可以使用到上述的索引来快速排序,但是对于 date 字段,仍然需要遍历所有 date 值然后进行排序操作;

Prefixes (前缀字段匹配)

什么是 Prefixes?看下面这个例子,假设我们有如下的复合索引

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

下面的两种 document 的表达方式都表示 Prefixes;

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

MongoDB 支持对仅使用到 Prefixes 的查询命中该复合索引,所以,还是以上述的复合索引为例,下面的三种情况都会命中该索引;

  • 如果查询中仅使用到 item,
  • 如果查询中同时使用到 item 和 location,且顺序是从左至右;
  • 如果查询中同时使用到 item、location 以及 stock,且顺序是从左至右;

但是,如果不是按照 Prefixes 的顺序来进行查询的,下面的方式将不会命中该索引,

  • 如果查询中仅使用到 location
  • 如果查询中仅使用到 location
  • 如果查询中同时使用到 location 和 stock 两个字段,且顺序是从左至右

Multikey Index

MongoDB 可以为某个数组字段的每一个元素建立索引,假设我们为 addr 的数组字段上的每一个元素建立索引,可以用下图表示,

图中蓝色方框代表的就是该为数组元素所建立的索引,从小到达;MongoDB 不但可以在原始数据类型(比如字符或者数字)的数组上建立索引同时也可以为由嵌入式文档所组成的数组元素而构建索引;

创建数组索引

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

创建索引的时候,MongoDB 会自动根据 field 的类型来判断是否创建 Multikey Index,如果类型是数组,那么将会自动创建数组索引;

Multikey Index Bounds

在对数组索引进行查询的时候,MongoDB 会对查询的条件进行相应的优化,详情参考 MongoDB 基础系列十八:索引之 Multikey Index Bounds

限制

复合数组索引( Compound Multikey Indexes )

不能同时在两个数组字段上建立一个复合索引,比如,我们有如下的 document 构成的 collection,

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

那么就不能在字段 a 和字段 b 上同时创建 Compound Indexes;

不能用作分片主键

不能使用数组索引( Multikey Indexes )来做为分片主键( Sharded Key );

不能用作 Hashed Indexes

如题;

完整匹配整个数组元素的限制

如果查询条件是需要完整匹配整个数组的元素,包含顺序;那么这个时候,数组索引只能作用到第一个查询元素上,它会查询所有的数组中包含该元素的数组,然后再从这些数组中依次匹配,找到完全匹配的数组项;比如,我们有如下的数据,

1
2
3
4
5
{ _id: 5, type: "food", item: "aaa", ratings: [ 5, 8, 9 ] }
{ _id: 6, type: "food", item: "bbb", ratings: [ 5, 9 ] }
{ _id: 7, type: "food", item: "ccc", ratings: [ 9, 5, 8 ] }
{ _id: 8, type: "food", item: "ddd", ratings: [ 9, 5 ] }
{ _id: 9, type: "food", item: "eee", ratings: [ 5, 9, 5 ] }

在 ratings 字段上创建数组索引,

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

通过下面的查询,我们想想要完整匹配 [5, 9] 数组;

1
db.inventory.find( { ratings: [ 5, 9 ] } )

首先,MongoDB 会查找 ratings 数组中包含 5 的所有 ratings 子集,然后从这些子集中依次去判断是否完整匹配 [5, 9];

地理空间索引( Geospatial Index )

TODO…

文本索引( Test Indexes )

TODO…

Hashed Indexes

TODO…

索引变种

唯一索引( Unique Indexes )

在单个字段上创建唯一索引

1
db.members.createIndex( { "user_id": 1 }, { unique: true } )

在多个字段上创建唯一索引( 称为 Unique Compound Index )

1
db.members.createIndex( { groupNumber: 1, lastname: 1, firstname: 1 }, { unique: true } )

部分索引( Partial Indexes )

MongoDB 天生就是为大数据场景应用所创建的数据库引擎;但是,有时候,如果对所有的数据都创建索引,非常浪费资源而且可能会导致性能问题;所以,通过 Partial Indexes 可以通过对需要被索引的字段设置过滤条件,进而只在该字段的部分数据集上创建索引,有针对性的提升查询性能;可以使用的过滤条件有,

  • \$eq
  • \$exists: true
  • \$gt, \$gte, \$lt, \$lte
  • \$type
  • \$and

比如,如下,我们创建了一个只对 ratings > 5 的 Partial Indexes ( 包含两个字段 cuisine:1 和 name: 1 );

1
2
3
4
db.restaurants.createIndex(
{ cuisine: 1, name: 1 },
{ partialFilterExpression: { rating: { $gt: 5 } } }
)

上面的索引的意思是,创建了一个复合索引 [ cuisine: 1, name: 1 ],但是该索引仅对 rating > 5 的条件下生效;

假如我们有如下的查询,

1
db.restaurants.find( { cuisine: "Italian", name: "Pawl", rating: { $gte: 8 } } )

那么,该 partial index 将会被命中,因为 rating: { $gte: 8 } 满足 Partial 的条件 { rating: { $gt: 5 } };

然而,下面这个查询将不会命中上述的 partial index

1
db.restaurants.find( { cuisine: "Italian", rating: { $lt: 8 } } )

Case Insensitive Indexes

正如该索引的名字那样,该索引并不会区分其大小写;

在创建索引的时候通过指定 collation 参数作为条件,来创建 Case Insensitive Indexes

1
2
3
4
5
6
db.collection.createIndex( { "key" : 1 },
{ collation: {
locale : <locale>,
strength : <strength>
}
} )

To specify a collation for a case sensitive index, include:

  • locale: specifies language rules. See Collation Locales for a list of available locales.
  • strength: determines comparison rules. A value of 1 or 2 indicates a case insensitive collation.

一个 case insensitive index 的例子

1
2
3
4
db.createCollection("fruit")

db.fruit.createIndex( { type: 1},
{ collation: { locale: 'en', strength: 2 } } )

通过 collation 设置,我们在 fruit collection 的 type 字段上上创建了一个不区分大小写的索引,

假设,我们创建了如下的测试数据,

1
2
3
db.fruit.insert( [ { type: "apple" },
{ type: "Apple" },
{ type: "APPLE" } ] )

考虑如下的查询情况

查询 1

1
db.fruit.find( { type: "apple" } ) // does not use index, finds one result

该查询不会使用到索引,只返回一个结果 apple,表示,是 case sensitive 的查询;

查询 2

1
2
db.fruit.find( { type: "apple" } ).collation( { locale: 'en', strength: 2 } )
// uses the index, finds three results

使用到了索引,并且返回三个结果,表示,是 case insensitive 的查询;

查询 3*

1
2
db.fruit.find( { type: "apple" } ).collation( { locale: 'en', strength: 1 } )
// does not use the index, finds three results

不会使用索引,返回三个结果,但仍表示,是 case insensitive 的查询;所以,这里可以知道,如果要使用 case insensitive 的索引,strength 参数值必须要匹配;

稀疏索引( Sparse Indexes )

我们知道,MongoDB 中同一个 collection M 中的不同 document 的结构是随性的,是可以不相同的,document A 可以包含 Field X 但是 document B 可以不包含 Field X,包含的却是另外一个字段 Field X;所以,假设,我们按照常规索引的方式对 Field X 创建索引,这个时候,MongoDB 会对整个 collection M 中的记录创建索引,当 document B 不存在该字段 Field X 的时候,会使用 X = null 的方式为其同样的创建索引;这样的话,就造成了不必要的空间浪费,所以,稀疏索引既是 Sparse Indexes 诞生了,它诞生的目的就是为了解决上述的情况,当 document B 不存在 Field X 的时候,直接将该记录跳过,不为该记录其创建任何索引;

Avaliable in version 3.2 or later

创建一个稀疏索引,

1
db.addresses.createIndex( { "xmpp_id": 1 }, { sparse: true } )

该索引将不会对不包含 xmpp_id 字段的 document 创建索引;

生死索引( Time To Alive Indexes, TTL Indexes )

TTL index 是在某个日期字段上所创建的一种索引,其作用是,为其设置声明时间,如果超过了声明时间,那么 MongoDB 将会自动的去删除该记录( document );

如何创建?

1
db.eventlog.createIndex( { "lastModifiedDate": 1 }, { expireAfterSeconds: 3600 } )

可以看到,通过 expireAfterSeconds 条件,我们在一个日期字段 lastModifiedDate 上创建了这么一个生死索引,在 3600 秒以后,自动销毁;(注意,单位是);

需要注意的是,也可以在一个不是 Date 类型的字段上创建 TTL index,但是,生死索引便不会生效;

References

http://learnmongodbthehardway.com/schema/indexes/
https://docs.mongodb.com/manual/indexes/