对文档 https://docs.mongodb.com/manual/indexes/index.html 的翻译整理,有个别段落已忽略。
索引
索引让MongoDB有效的执行查询语句,没有索引MongoDB必须对collection中的每个文档扫描来选择复合条件的条件的。如果对于一个查询存在合适的索引,MongoDB能利用索引限制检查的文档数。
索引是一种特殊的数据结构,以简单的遍历形式存储数据集合中的一小部分。索引存储特定字段或者字段的集合,以字段的值排序。索引的排序支持高效的等值查询和范围查询。除此之外,MongoDB能利用索引的排序返回排序后结果集。
默认的 _id 索引
MongoDB在collection创建时为 _id 字段创建唯一索引, _id 索引阻止客户端插入 _id 值相同的文档。无法删除 _id 字段上的索引。
创建索引
db.collection.createIndex( { name: -1 } )
索引类型
单字段索引
{ score:1 }
复合索引
{ userid: 1, score: -1 }
多键索引
db.collection.createIndex( { size.height : -1 } )
单字段索引
示例文档数据如下:
{
“_id”: ObjectId(“570c04a4ad233577f97dc459”),
“score”: 1034,
“location”: { state: “NY”, city: “New York” }
}
对单个字段创建降序索引
为score 创建索引
db.records.createIndex( { score: 1 } )
对内嵌字段创建索引(使用符号“.”)
为location.state字段创建索引
db.records.createIndex( { “location.state”: 1 } )
创建的索引支持以下查询
db.records.find( { “location.state”: “CA” } )
db.records.find( { “location.city”: “Albany”, “location.state”: “NY” } )
为内嵌文档创建索引
db.records.createIndex( { location: 1 } )
如下查询能用到location字段的索引
db.records.find( { location: { city: “New York”, state: “NY” } } )
复合索引
示例文档:
{
“_id”: ObjectId(…),
“item”: “Banana”,
“category”: [“food”, “produce”, “grocery”],
“location”: “4th Street Store”,
“stock”: 4,
“type”: “cases”
}
创建复合索引
命令:
db.collection.createIndex( { : , : , … } )
为 item 以及 stock 创建索引:
db.products.createIndex( { “item”: 1, “stock”: 1 } )
复合索引中字段顺序是重点,上例中将先按照 item 的值对文档引用排序,然后对符合item 条件的文档按照 stock 的值排序。
除了支持所有索引字段的查询外,复合索引还支持前缀匹配查询。意思就是,{ “item”: 1, “stock”: 1 } 支持对单个 item 字段的查询,也支持对 item 和 stock 两个字段的查询。
db.products.find( { item: “Banana” } )
db.products.find( { item: “Banana”, stock: { $gt: 5 } } )
排序
索引以升序(1) 或降序(-1)存储对字段的引用。对于单字段索引,索引的顺序无关紧要,因为MongoDB可在任一顺序遍历。但是对于复合索引,排序顺序关乎到索引能否支持排序操作。
一个 collection 其文档字段有 username 和 date 。如下索引:
db.events.createIndex( { “username” : 1, “date” : -1 } )
可以支持以下两种查询方式:
db.events.find().sort( { username: 1, date: -1 } )
db.events.find().sort( { username: -1, date: 1 } )
但不支持以下查询方式:
db.events.find().sort( { username: 1, date: 1 } )
前缀
索引前缀是索引字段开头的子集,有如下复合索引:
{ “item”: 1, “location”: 1, “stock”: 1 }
则有如下索引前缀:
- { item: 1 }
- { item: 1, location: 1 }
对于如下字段的查询可以用到索引:
- item 字段
- item 字段 和 location 字段
- item 字段 和 location 字段 和 stock 字段
如下查询无法用到索引:
- location 字段
- location 字段,或者…
- location 字段 和 stock 字段
多键索引
创建多键索引
使用db.collection.createIndex() 创建索引:
db.coll.createIndex( { : < 1 or -1 > } )
若添加索引的字段是一个数组,MongoDB 自动创建多键索引;不需要明确指定索引类型。
索引边界
如果一个索引是多键索引,索引边界的计算遵循特殊的规则。多键索引边界的详细信息,参看 Multikey Index Bounds.
唯一多键索引
对于唯一索引适用于集合中的单独文档,而不是单个文档之中。
由于唯一约束应用于单独文档,对于唯一多键索引,只要文档的索引键值不重复其他文档的索引键值,则对于唯一的多键索引,文档可能具有导致重复索引键的数组元素。
更多信息,参看Unique Constraint Across Separate Documents.
限制
复合多键索引
对于一个复合多键索引,每个建立过索引的文档最多有一个值是数组的字段能做索引。意思就是:
- 不能为多个值为数组的字段创建多键复合索引。例如:
{ _id: 1, a: [ 1, 2 ], b: [ 1, 2 ], category: “AB - both arrays” }
你不能创建一个复合多键索引{a:1, b:1},因为a、b字段的值都是数组
当一个复合多键索引已存在,你不能插入一个文档,违反此规则。
例如某collection有如下数据:
{ _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字段的值都是数组,那么MongoDB会插入失败。
如果一个字段是文档数组,可以为内嵌字段添加索引来创建复合索引。
例如:
{ _id: 1, a: [ { x: 5, z: [ 1, 2 ] }, { z: [ 1, 2 ] } ] }
{ _id: 2, a: [ { x: 5 }, { z: 4 } ] }
可以创建复合索引{“a.x”:1, “a.z”:1}。依然符合限制:最多一个字段的值是数组。
共享索引
不能指定多键索引,作为共享索引
哈希索引
哈希索引不能做多键索引
覆盖查询
多键索引不能覆盖对数组字段的查询
整体查询数组字段
当查询过滤器为整个数组指定一个特定的匹配条件时,MongoDB能运用多键索引查找数组的第一个元素,但是不运用多键索引扫描查找整个数组。
例如:
{ _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字段上有多键索引
db.inventory.createIndex( { ratings: 1 } )
如下查询查找ratings字段为[5, 9] 的文档:
db.inventory.find( { ratings: [ 5, 9 ] } )
MongoDB能运用多键索引找到在ratings数组任一位置有5的文档,然后MongoDB检索过滤这些文档,找到ratings字段等于[5, 9] 的文档。
例子
索引基本数组
考虑survey collection 有如下文档:
{ _id: 1, item: “ABC”, ratings: [ 2, 5, 9 ] }
在ratings 字段创建索引
db.survey.createIndex( { ratings:1 } )
因为ratings 字段包含数组在ratings上的索引为多键索引。该多键索引包含如下三个索引键,每个都指向相同的文档。
- 2,
- 5,and
- 9,
索引内嵌文档的数组
可为包含嵌套对象的数组创建多键索引
考虑inventory collection 有如下文档:
>
{
_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 }
]
}
…
>
创建索引:
db.inventory.createIndex( { “stock.size”: 1, “stock.quantity”: 1 } )
复合多键索引既支持两个字段的查询也支持索引前缀”stock.size”,例如:
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会尝试
通过求交集或者并集来获取索引边界。
多键索引交叉边界
交叉边界是多个边界的逻辑连接。如给出两个边界[ [ 3, Infinity ] ] 和 [ [ -Infinity, 6 ] ] 边界的交集为:[ [ 3, 6 ] ]。
给出一个索引数组,考虑一个查询指定该数组的多个字段,并且可以使用多键索引。如果一个$elemMatch 连接查询字段,MongoDB能交叉多键索引的边界
比如:survey collection 包含一个字段item 和一个数组字段ratings:
{ _id: 1, item: “ABC”, ratings: [ 2, 9 ] }
{ _id: 2, item: “XYZ”, ratings: [ 4, 3 ] }
在ratings创建一个多键索引:
db.survey.createIndex( { ratings: 1 } )
如下使用$elemMatch 来查找数组只要包含一个元素符合两个条件
db.survey.find( { ratings : { $elemMatch: { lte: 6 } } } )
分别考虑查询字段:
- 大于或者等于3的边界是[ [ 3, Infinity ] ];
- 小于或等于6的边界是[ [ -Infinity, 6 ] ];
因为查询语句使用$elemMatch 连接查询谓词,MongoDB能交叉边界:
ratings: [ [ 3, 6 ] ]
如果查询语句不使用$elemMatch连接条件,MongoDB不能交叉多键索引的边界,考虑如下查询:
db.survey.find( { ratings : { lte: 6 } } )
查询语句查找ratings数组至少有一个大于等于3且小于等于6的元素,因为单个元素不需要
满足两个标准,MongoDB不会交叉边界,而是使用 [ [ 3, Infinity ] ] 或 [ [ -Infinity, 6 ] ]。MongoDB不保证会使用哪一个边界。
多键索引的复合边界
复合边界指为复合索引的多个键使用边界,比如给出一个复合索引{ a:1, b:1 } ,a字段的边界[ [ 3, Infinity ] ],b字段的边界[ [ -Infinity, 6 ] ] 复合边界导致两个边界都使用:
{ a: [ [ 3, Infinity ] ], b: [ [ -Infinity, 6 ] ] }
如果MongoDB不能复合两个边界,MongoDB总是通过前面字段的边界约束索引扫描,在本例中就是a: [ [ 3, Infinity ] ]。
数组字段上的复合索引
考虑复合多键索引,一个复合索引其中一个索引字段的值是数组。例如一个collection survey 的文档中包含item字段和ratings:
{ _id: 1, item: “ABC”, ratings: [ 2, 9 ] }
{ _id: 2, item: “XYZ”, ratings: [ 4, 3 ] }
在item和ratings字段上建立复合索引:
db.survey.createIndex( { item: 1, ratings: 1 } )
以下查询指定索引的两个键:
db.survey.find( { item: “XYZ”, ratings: { $gte: 3 } } )
分开考虑查询字段:
item: “XYZ” 的边界是[ [ “XYZ”, “XYZ” ] ];
ratings: { $gte: 3 } 的边界是 [ [ 3, Infinity ] ];
MongoDB能够复合两个边界使用联合边界:
{ item: [ [ “XYZ”, “XYZ” ] ], ratings: [ [ 3, Infinity ] ] }
标量索引字段上的范围查询
从MongoDB3.4开始,MongoDB跟踪哪些字段会使得索引成为多键索引。跟踪这个信息允许MongoDB查询引擎使用更加松散的边界。
前面提到的复合索引是在标量字段item和数组字段ratings:
db.survey.createIndex( { item: 1, ratings: 1 } )
对于WiredTiger 和In-Memory 存储引擎如果一个查询操作指定复合多键索引(大于等于3.4版本创建)的一个标量字段,MongoDB会交叉字段的边界。
例如,如下查询中指定一个标量字段的查询范围和一个数组字段的查询范围。
db.survey.find( {
item: { lte: “Z”}, ratings : { $elemMatch: { lte: 6 } }
} )
>
MongoDB会交叉边界,对于item来说就是[ [ “L”, “Z” ] ] 而对于ratings是 [[3.0, 6.0]]:
“item” : [ [ “L”, “Z” ] ], “ratings” : [ [3.0, 6.0] ]
对于另一个例子,考虑标量字段是内嵌的,比如:
{ _id: 1, item: { name: “ABC”, manufactured: 2016 }, ratings: [ 2, 9 ] }
{ _id: 2, item: { name: “XYZ”, manufactured: 2013 }, ratings: [ 4, 3 ] }
为”item.name”,”item.manufactured”和数组字段ratings:
db.survey.createIndex( { “item.name”: 1, “item.manufactured”: 1, ratings: 1 } )
对于如下查询:
db.survey.find( {
“item.name”: “L” ,
“item.manufactured”: 2012
} )
MongoDB使用复合边界:
“item.name” : [ [“L”, “L”] ], “item.manufactured” : [ [2012.0, 2012.0] ]
更早一些的版本无法复合这些标量字段的边界
标量字段的范围查询(MMAPv1)
对于MMAPv1存储引擎,MongoDB不能复合多键索引中标量字段的边界,即使仅仅是查询标量字段。
内嵌文档数组中字段的复合索引
如果一个数组包含内嵌文档,为了给包含内嵌文档的字段加索引,使用”.字段名”的方式,比如给出如下内嵌文档:
ratings: [ { score: 2, by: “mn” }, { score: 9, by: “anon” } ]
“.字段名”的方式对于score来说就是”ratings.score”
非数组字段和数组中字段的复合边界
考虑collection survey2中的如下文档:
{
_id: 1,
item: “ABC”,
ratings: [ { score: 2, by: “mn” }, { score: 9, by: “anon” } ]
}
{
_id: 2,
item: “XYZ”,
ratings: [ { score: 5, by: “anon” }, { score: 7, by: “wv” } ]
}
在非数组字段item和ratings.score以及ratings.by上创建一个复合索引:
db.survey2.createIndex( { “item”: 1, “ratings.score”: 1, “ratings.by”: 1 } )
如下查询指定所有三个字段的查询条件:
db.survey2.find( { item: “XYZ”, “ratings.score”: { $lte: 5 }, “ratings.by”: “anon” } )
分开考虑:
- 对于item: “XYZ” 的边界是 [ [ “XYZ”, “XYZ” ] ];
- 对于score: { $lte: 5 } 的边界是 [ [ -Infinity, 5 ] ];
- 对于by: “anon” 的边界是 [ “anon”, “anon” ]
MongoDB能复合item 、ratings.score和ratings.by的边界,MongoDB不保证的选用哪一个字段的边界,例如MongoDB复合item和ratings.score的边界:
{
“item” : [ [ “XYZ”, “XYZ” ] ],
“ratings.score” : [ [ -Infinity, 5 ] ],
“ratings.by” : [ [ MinKey, MaxKey ] ]
}
或者MongoDB也可能选择复合item和ratings.by的边界:
{
“item” : [ [ “XYZ”, “XYZ” ] ],
“ratings.score” : [ [ MinKey, MaxKey ] ],
“ratings.by” : [ [ “anon”, “anon” ] ]
}
但是复合ratings.score和ratings.by的边界,查询语句必须使用$elemMatch
数组中字段的复合索引边界
为了复合同一数组中的键的边界:
- 索引键必须共享相同的字段路径,查询必须在路径的上字段指定使用$elemMatch
对于内嵌文档上的字段,”.字段名”,比如”a.b.c.d”,是d的字段路径,为了复合来自同一数组的索引键的边界,$elemMatch 必须在在这样路径的字段名上”a.b.c”。
比如在ratings.score和ratings.by字段上:
db.survey2.createIndex( { “ratings.score”: 1, “ratings.by”: 1 } )
“ratings.score” 和”ratings.by”共享ratings路径,如下查询在ratings上使用$elemMatch来查找至少有一个元素复合两个条件的文档:
db.survey2.find( { ratings: { $elemMatch: { score: { $lte: 5 }, by: “anon” } } } )
分开考虑:
- score: { $lte: 5 } 的边界是 [ -Infinity, 5 ];
- by: “anon” 的边界是 [ “anon”, “anon” ]
MongoDB能复合两个边界:
{ “ratings.score” : [ [ -Infinity, 5 ] ], “ratings.by” : [ [ “anon”, “anon” ] ] }
没有$elemMatch的查询
如果查询不在索引数组的条件中加入$elemMatch,MongoDB不能复合边界。考虑如下查询:
db.survey2.find( { “ratings.score”: { $lte: 5 }, “ratings.by”: “anon” } )
因为数组中的单个内嵌文档不需要复合两个标准,MongoDB不会复合边界。当使用一个复合索引,如果MongoDB不能约束素有的索引字段,MongoDB总是约束索引中开头字段,本例总就是”ratings.score”
{
“ratings.score”: [ [ -Infinity, 5 ] ],
“ratings.by”: [ [ MinKey, MaxKey ] ]
}
$elemMatch 在不完整路径上
如果查询不在内嵌字段的路径上指定$elemMatch,MongoDB不能复合来自同一数组的索引字段的边界
例如:collection survey3包含文档:
{
_id: 1,
item: “ABC”,
ratings: [ { scores: [ { q1: 2, q2: 4 }, { q1: 3, q2: 8 } ], loc: “A” },
{ scores: [ { q1: 2, q2: 5 } ], loc: “B” } ]
}
{
_id: 2,
item: “XYZ”,
ratings: [ { scores: [ { q1: 7 }, { q1: 2, q2: 8 } ], loc: “B” } ]
}
创建索引:
db.survey3.createIndex( { “ratings.scores.q1”: 1, “ratings.scores.q2”: 1 } )
字段”ratings.scores.q1”和 “ratings.scores.q2”共享字段路径”ratings.scores” $elemMatch必须在该路径上。
如下查询使用了$elemMatch但是不在需要的路径上:
db.survey3.find( { ratings: { $elemMatch: { ‘scores.q1’: 2, ‘scores.q2’: 8 } } } )
像这样的情况MongoDB不能复合边界,”ratings.scores.q2”字段在索引扫描时不能被约束,为了复合边界,查询必须在”ratings.scores”路径上使用$elemMatch:
db.survey3.find( { ‘ratings.scores’: { $elemMatch: { ‘q1’: 2, ‘q2’: 8 } } } )