MongoDB 中索引因为 $or
引发过多次问题,最近又有新发现,所以决定趁此机会,将发现总结在这里。
综述
规则一:正如 文档 表述,$or
数组中的每一个对象都应当能完整地命中一个索引。
规则二:用于 $or
内的字段同时出现在与 $or
并列的位置,且字段是索引人关键字段时,会影响索引的命中情况。
详细解释
MongoDB 版本: 3.2.x
规则一:正如 文档 表述,$or
数组中的每一个对象都应当能完整地命中一个索引。
比如,当你有索引:
db.getCollection('membershipDiscount').createIndex({"accountId": 1, "member.phone": 1})
db.getCollection('membershipDiscount').createIndex({"accountId": 1, "member.name": 1})
此时,你需要用以下这种方式:
db.getCollection('membershipDiscount').find({
"$or": [
{
"accountId": ObjectId("58d2112d829fa700204bbcc4"),
"member.phone": "SearchKey"
},
{
"accountId": ObjectId("58d2112d829fa700204bbcc4"),
"member.name": "SearchKey"
}
],
}).explain('executionStats')
它执行结果的 winningPlan
是这样子的:
"winningPlan" : {
"stage" : "SUBPLAN",
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "OR",
"inputStages" : [
{
"stage" : "IXSCAN",
"keyPattern" : {
"accountId" : 1,
"member.phone" : 1
},
"indexName" : "accountId_1_member.phone_1",
"indexBounds" : {
"accountId" : [
"[ObjectId('58d2112d829fa700204bbcc4'), ObjectId('58d2112d829fa700204bbcc4')]"
],
"member.phone" : [
"[\"SearchKey\", \"SearchKey\"]"
]
}
// ...
},
{
"stage" : "IXSCAN",
"keyPattern" : {
"accountId" : 1,
"member.name" : 1
},
"indexName" : "accountId_1_member.name_1",
"indexBounds" : {
"accountId" : [
"[ObjectId('58d2112d829fa700204bbcc4'), ObjectId('58d2112d829fa700204bbcc4')]"
],
"member.name" : [
"[\"SearchKey\", \"SearchKey\"]"
]
}
// ...
}
]
}
}
},
但在使用时,在此基础上又发现了一个规则,即我们用如下这种方式时:
db.getCollection('membershipDiscount').find({
"$or": [
{
"accountId": ObjectId("58d2112d829fa700204bbcc4"),
"member.phone": "SearchKey"
},
{
"accountId": ObjectId("58d2112d829fa700204bbcc4"),
"member.name": "SearchKey"
}
],
"accountId": ObjectId("58d2112d829fa700204bbcc4"),
}).explain('executionStats')
它命中的索引只有一个 accountId, member.name
,其 winningPlan
是这样子的:
"winningPlan" : {
"stage" : "FETCH",
"filter" : {
// ...
},
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"accountId" : 1,
"member.name" : 1
},
"indexName" : "accountId_1_member.name_1",
"isMultiKey" : false,
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 1,
"direction" : "forward",
"indexBounds" : {
"accountId" : [
"[ObjectId('58d2112d829fa700204bbcc4'), ObjectId('58d2112d829fa700204bbcc4')]"
],
"member.name" : [
"[MinKey, MaxKey]"
]
}
}
},
而后,再将索引搜索的结果再逐个检查(keysExamined
)每个 document 是否命中 member.phone
这个条件。
也就是说, 规则二:用于 $or
内的字段同时出现在与 $or
并列的位置,且字段是索引人关键字段时,会影响索引的命中情况。
那导致这个索引只命中一个的原因,会不会是因为命中了 $or
并列字段的索引,而不是 $or
的子句的索引,它命中索引会不会首选是外层条件能命中的索引。我又做了个实验,用一个表创建了三个索引:
db.getCollection('couponCode').createIndex({accountId: 1, status: 1})
db.getCollection('couponCode').createIndex({code: 1})
db.getCollection('couponCode').createIndex({lowerCode: 1})
然后用查询:
db.getCollection('couponCode').find({
accountId: ObjectId('5c552c919cd0b306b62fbc82'),
$or: [
{code: "2t9s6df3b9jf4fdfsjqxwkso"},
{lowerCode: "2t9s6df3b9jf4fdfsjqxwkso"}
]
}).explain('executionStats')
发现其 winningPlan
为:
"winningPlan" : {
"stage" : "FETCH",
"filter" : {
"accountId" : {
"$eq" : ObjectId("5c552c919cd0b306b62fbc82")
}
},
"inputStage" : {
"stage" : "OR",
"inputStages" : [
{
"stage" : "IXSCAN",
"keyPattern" : {
"code" : 1.0
},
"indexName" : "code_1",
// ...
"indexBounds" : {
"code" : [
"[\"2t9s6df3b9jf4fdfsjqxwkso\", \"2t9s6df3b9jf4fdfsjqxwkso\"]"
]
}
},
{
"stage" : "IXSCAN",
"keyPattern" : {
"lowerCode" : 1.0
},
"indexName" : "lowerCode_1",
// ...
"indexBounds" : {
"lowerCode" : [
"[\"2t9s6df3b9jf4fdfsjqxwkso\", \"2t9s6df3b9jf4fdfsjqxwkso\"]"
]
}
}
]
}
},
即在规则二中,与 $or
并列有匹配条件不是直接原因,我们在项目中也常常用这种条件去查询,仅 $or
内的子匹配字段与 $or
外的重复且就索引的关键字段时才会有这种情况。
在此,顺便补充一下 $regex
模糊查询对索引的利用效率:
- 加
i
option(case insensitive) 的是无法利用索引的。(解决方法是存储时冗余一个存小写值的字段,查询时转换为小写查) - 性能从高到低:
/^a/
、/^a.*/
、/^a.*$
、/a/
。 -
文档 中有明确解释,说:“虽然
/^a/
、/^a.*/
、/^a.*$
等价于相同的字符串匹配,但它们的性能是不一样的”。(注:.
用于匹配除换行符\n
外的任何单字符)
网友评论