什么是多键索引
根据官方定义,多键索引的含义是,如果你对文档中的一个数组的键创建了一个索引,那么MongoDB就会为数组中的每一个元素创建一个索引,这些多键索引支持针对数组字段的高效查询,多键索引不仅仅创建在包含基本类型值的数组,还可以创建在包含嵌套文档的数组中。
如果没有很明白多键索引的意思,那么下面我将演示一个通俗的例子,这里有两个如下结构的文档:
{
"_id" : ObjectId("5837080221f5ea1660d59910"),
"student_name" : "student_6324",
"score" : [ 85, 98, 67 ]
}
{
"_id" : ObjectId("573293aeb2f1074cbcf082a0"),
"student_name" : "student_4396",
"score" : [ 67, 53, 42 ]
}
当我进行如下条件查询的时候,将会得到不同的结果集:
{"score" : 85} → {"_id" : ObjectId("5837080221f5ea1660d59910")}
{"score" : 42} → {"_id" : ObjectId("573293aeb2f1074cbcf082a0")}
{"score" : 67} → [{"_id" : ObjectId("5837080221f5ea1660d59910")}, {"_id" : ObjectId("573293aeb2f1074cbcf082a0")} ]
这样的索引,就是多键索引。
索引边界
如果一个索引是多键索引,那么在计算索引边界时需要遵守一些特殊的规则。
索引边界规定着一个查询过程中索引查询的部分,当索引上存在多个条件时,MongoDB则会尝试通过交叉或者复合的形式来组合边界,目的是为了创建出一个较小边界的遍历。
边界交叉
边界交叉指的是多个边界的逻辑上的交集。MongoDB可以使用$elemMatch来创建连接多个查询条件的交集。
接下来我们举一个边界交叉的例子。
还是这两个文档:
{
"_id" : ObjectId("5837080221f5ea1660d59910"),
"student_name" : "student_6324",
"score" : [ 85, 98, 67 ]
}
{
"_id" : ObjectId("573293aeb2f1074cbcf082a0"),
"student_name" : "student_4396",
"score" : [ 67, 53, 42 ]
}
我们创建一个score键的索引:
db.achievements.createIndex({"score" : 1})
然后我们进行如下查询:
db.achievements.find({"score" : {"$elemMatch" : {"$gte" : 50, "$lte" : 100}}})
在你的世界里,通过大脑快速度的运算,边界值区间应该是[50, 100]。
但是,机器会把这个过程细化,具体步骤如下:
- 第一个查询条件{“$gte” : 50}转换为[50, Infinity]。
- 第二个查询条件{“$lte” : 100}转换为[-Infinity, 100]。
由于使用了$elemMatch,MongoDB将两个查询条件进行交集,于是查询边界变为了[50, 100]。
如果在查询条件中没有使用$elemMatch,MongoDB不会对多键索引边界进行交叉操作,查询条件如下所示:
db.achievements.find({"score" : {"$gte" : 50, "$lte" : 100}})
由于在查询条件中没有使用$elemMatch,每个查询条件不需要知道其他查询条件的范围,MongoDB就没有对索引边界进行交叉,并且它会使用两个条件中的一个,MongoDB不保证它会使用两个条件中的哪一个。
多键索引的复合边界
在数组字段上使用复合索引
在如下结构的文档中:
{
"_id" : ObjectId("5837080221f5ea1660d59910"),
"student_name" : "student_6324",
"score" : [ 85, 98, 67 ]
}
创建一个复合索引:
db.achievements.createIndex({"student_name" : 1, "score" : 1})
形如这样的索引就是一个创建数组字段上的复合索引。
接着我们进行一次根据复合索引的查询操作。
db.achievemnts.find({"student_name" : "student_6324", "score" : {"$gte" : 60}})
在这个索引查询条件过程中,MongoDB进行了如下查询范围并集操作:
- “student_name” : “student_6324″的范围转换为[“student_6324 “, “student_6324 “]。
- “score” : {“$gte” : 60}的范围转换为[60, Infinity]。
MongoDB会将两个范围组合成为一个范围:
{"student_name" : [["student_6324", "student_6324"]], score : [[60, Infinity]]}
在内嵌文档上创建复合索引
这种索引类型的索引字段分别来自于非数组和数组字段
我们继续创建一个新的集合,集合中文档结构如下:
{
"_id" : ObjectId("5837080221f5ea1660d59910"),
"student_name" : "student_6324",
"achievement" : [
{"subject" : "Math", "score" : "137"},
{"subject" : "English", "score" : "142"}
]
}
接下来我们对非数组字段以及数组字段创建索引:
db.achievement_detail.createIndex({"student_name" : 1, "achievement.subject" : 1, "achievement.score" " : 1})
然后我们来做一个实验:
db.achievement_detail.find({"student_name" : "student_6324", "achievement.subject" : "English", "achievement.score" " : {"$gte" : 90}})
MongoDB进行的范围转换如下
- {“student_name” : “student_6324”}转换为[[“student_6324”, “student_6324”]]。
- {“achievement.subject” : “English”}转换为[[“English” , “English”]]。
- {“achievement.score” ” : {“$gte” : 90}}转换为[[90, Infinity]]。
MongoDB可以将student_name的查询条件和achievement.subject与achievement.score这两个查询条件中的一个进行范围合并,这取决于查询谓语或者是索引键值。MongoDB无法保证哪个查询条件将会与student_name组合在一起。所以,组合后的范围会出现以下两种情况:
情况一:
{
"student_name" : [["student_6324", "student_6324"]],
"achievement.subject" : [["English" , "English"]],
"achievement.score" " : [[MinKey, MaxKey]]
}
情况二:
{
"student_name" : [["student_6324", "student_6324"]],
"achievement.subject" : [[MinKey, MaxKey]],
"achievement.score" " : [[90, Infinity]]
}
如果你想将achievement.subject和achievement.score组合一起来,一起出现在查询条件中,则需要使用$elemMatch。
对一个数组进行索引索引范围组合
如果想要组合一个数组中的多个索引键的查询范围,索引的键必须共用相同的字段路径,但是不包括字段名称,并且查询条件必须在这个字段路径上指定$elemMatch谓语。
字段路径,举个例子,在键achievement.subject中,achievement就是subject的字段路径。
为了演示组合数组中多个字段的索引范围,我们创建一个适当的索引:
db.achievement_detail.createIndex({"achievement.subject" : 1, "achievement.score" : 1})
两个索引的具有相同的字段路径achievement,所以满足组合索引范围的条件,然后我们使用$elemMatch谓语来组合索引范围:
db.achievement_detail.find({"achievement" : {"$elemMatch" : {"subject" : "Math", "score" : {"$gte" : 90}}}})
经过组合步骤后,索引的范围被聚合为:
{"achievement.subject" : [["Math", "Math"]], "achievement.score" : [[90, Infinity]]}
不使用$elemMatch进行查询
如果没有使用$elemMatch连接多个索引在数组字段上的查询条件,MongoDB就无法对范围进行聚合,多个查询条件之间也完全不知道彼此的范围,这种情况下,MongoDB一般会使用索引中的第一个元素进行约束。
$elemMatch使用在不完整的路径
如果没有将完整的字段路径绑定在$elemMatch上,即使没有包含字段名称,MongoDB也不会对索引范围进行聚合,比如如下的文档结构。
{
"_id" : ObjectId("5837080221f5ea1660d59910"),
"student_name" : "student_6324",
"achievement" : [
{"subject" : "Math", "score" : {"choice": 45, "input" : 38}},
{"subject" : "English", "score" : {"choice" : 37, "input" : 42}}
]
}
我们来创建一个包含多个数组内部字段的索引:
db.achievement_detail.createIndex({"achievement.score.choice" : 1, "achievement.score.input" : 1})
如果使用以下的查询条件,MongoDB不会聚合索引范围:
db.achievement_detail.find({"achievement" : {"$elemMatch" : {"score.choice" : {"$gte" : 30}, "score.input" : "$gte" : 40}}})
所以,必须把数组索引字段的完全路径绑定在$elemMatch谓语上。
多个$elemMatch条件
截止到写下这篇文章为止,MongoDB暂时还不支持多个$elemMatch的组合操作,举个例子。
如果文档的数据结构如下所示:
{
"_id" : ObjectId("5837080221f5ea1660d59910"),
"student_name" : "student_6324",
"achievement" : [
{"subjects" : {"subject_one" : "Advanced Mathematics", "subject_two" : "Linear Algebra"}, "score" : {"score_one": 139, "score_two" : 128}},
{"subjects" : {"subject_one" : "First Year English", "subject_two" : "Second Year English"}, "score" : {"score_one" : 145, "score_two" : 142}}
]
}
接下来我们创建一个数组内部字段的索引:
db.achievement_detail.createIndex({"achievment.subjects.subject_one" : 1, "achievement.subjects.subject_two" : 1, "achievement.score.score_one" : 1, "achievement_score.score_two" : 1})
如果有形如下述的多个$elemMatch查询条件:
db.achievement_detail.find({"achievement.subjects" : {"$elemMatch" : {"subject_one" : "Advanced Mathematics", "subject_two" : "Linear Algebra"}}, "achievement.score" : {"$elemMacth" : {"score_one" : 145, "score_two" : 142}}})
经过转换后,两个$elemMatch的范围如下:
- {“achievement.subjects.subject_one” : [[“Advanced Mathematics”]], “achievement.subject.subject_two” : [[“Linear Algebra”]]}
- {“achievement.score.score_one” : [[145, 145]], “achievement.score.score_two” : [[142, 142]]}
虽然我们可以得出转化后的范围,但是其实,MongoDB无法将两个$elemMatch条件聚合在一起。
它只会将该索引中起始的字段进行$elemMatch连接,也就说,它只会聚合achievement.score.score_one和achievement.score.score_two两个索引范围。
限制
下面来BB一下多键索引的限制。
复合多键索引
对于一个复合多键索引来说,只能对一个数组字段进行索引,如果一个复合多键索引已经存在,你无法破坏它的约束而插入一条文档。
如果一个集合中的文档的结构如下所示:
{
"_id" : ObjectId("5837080221f5ea1660d59910"),
"student_name" : "student_6324",
"achievement" : [
"Advanced Mathematics",
"Linear Algebra"
],
"score" : [
"score_one": 139,
"score_two" : 128
]
}
那么我们无法创建一个如下的索引:
db.achievement_detail.createIndex({"achievement" : 1, "score" : 1})
但是如果你的文档结构是这样的:
{
"_id" : ObjectId("5837080221f5ea1660d59910"),
"student_name" : "student_6324",
"achievement" : "Math",
"score" : [
"score_one": 139,
"score_two" : 128
]
}
以及
{
"_id" : ObjectId("5837080221f5ea1660d59910"),
"student_name" : "student_6324",
"achievement" : [
"Advanced Mathematics",
"Linear Algebra"
],
"score" : 138
}
这样,你是可以创建一个基于achievement键和score键的索引:
db.achievement_detail.createIndex({"achievement" : 1, "score" : 1})
这你可能会有疑问,这样是不是就可以完美避开上一条限制,对两个数组字段进行索引?
答案是否定的,当插入的achievement和score都是数组类型的时候,MongoDB并不会插入该文档。
片键
在MongoDB 2.6版本之前,你是无法将一个多键索引指定为片键索引的。
但是在MongoDB 2.6版本之后,如果片键是一个复合索引的前缀,那么这个复合索引可以允许索引中不是片键的字段的部分变成一个多键索引。复合多键索引对性能方面会有一定的影响。
Hash索引
Hash索引无法成为多键索引。
覆盖查询
多键索引不支持覆盖查询。
什么是覆盖查询?
覆盖查询是指完全使用索引来满足查询,并且不会检查任何文档,如果,所有的查询字段都时一个索引的一部分,并且返回的结果集的字段都是同一个索引,那么,这就是一个覆盖查询。
对一个数组进行精确查找
如果需要指定一个数组,并对这个数组进行精确查找,MongoDB可以根据查询条件数组中第一个元素来定位文档,但是不能使用多键索引进行遍历来找到包含指定数组的文档。
如果已经找到和数组中第一个元素相匹配的文档,那么MongoDB将会检索结果集中,索引值等于指定数组的文档。
比如,一个集合的文档结构如下:
{
"_id" : ObjectId("5837080221f5ea1660d59910"),
"student_name" : "student_6324",
"achievement" : [
"Advanced Mathematics",
"Linear Algebra"
],
}
之后创建一个多键索引:
db.achievements_detail_exact_array.createIndex({"achievement" : 1})
当对多键索引进行一个精确查询时:
db.achievements_detail_exact_array.find({"achievement" : ["Advanced Mathematics", "Linear Algebra"]})
MongoDB会先对集合所有的文档中achievement字段的第一个值为”Advanced Mathematics”进行筛选,筛选完毕后,继续在结果集中判断achievement字段的值等于[“Advanced Mathematics”, “Linear Algebra”]的文档,最终得到查询条件的结果集。