$or 的每个分支各走哪个索引?一个分支没有索引会怎样?加了 sort 和别的条件之后呢?这篇用 20 万个文档和 explain 逐个情形验证。
test> db.t.find({$or: [{a: 5}, {b: 17}]})
{
plan: 'SUBPLAN > FETCH > OR[IXSCAN(a_1) | IXSCAN(b_1)]',
nReturned: 240,
keysExamined: 240,
docsExamined: 240,
ms: 9
}每个分支有可用索引时,$or 的计划是 OR[IXSCAN | IXSCAN],各自按自己的索引读,合并后再 FETCH 并去重。
test> db.t.find({$or: [{a: 5}, {d: 17}]}) // d 没有索引
{
plan: 'SUBPLAN > COLLSCAN',
nReturned: 202,
keysExamined: 0,
docsExamined: 200000,
ms: 42
}只要有一个分支用不上索引,整个 $or 退化为 COLLSCAN,检查全部 200,000 个文档。
test> db.t.find({c: 7, $or: [{a: 5}, {b: 17}]})
{
plan: 'FETCH > OR[IXSCAN(a_1) | IXSCAN(b_1)]',
nReturned: 0,
keysExamined: 240,
docsExamined: 240,
ms: 1
}
test> 建了 {c, a} 和 {c, b} 两个复合索引之后,同一个查询
{
plan: 'FETCH > OR[IXSCAN(c_1_a_1) | IXSCAN(c_1_b_1)]',
nReturned: 0,
keysExamined: 0,
docsExamined: 0,
ms: 2
}$or 之外的条件要靠对应的复合索引({c, a}、{c, b})分配进每个分支;只有单字段索引时是 FETCH 后过滤。
test> db.t.find({$or: [{a: 5}, {b: 17}]}).sort({d: 1}) // 有 {a, d} 和 {b, d}
{
plan: 'SUBPLAN > FETCH > SORT_MERGE[IXSCAN(a_1_d_1) | IXSCAN(b_1_d_1)]',
nReturned: 240,
keysExamined: 240,
docsExamined: 240,
ms: 8
}
test> 删掉 {b, d} 索引后同一个查询
{
plan: 'SUBPLAN > SORT > FETCH > OR[IXSCAN(b_1) | IXSCAN(a_1_d_1)]',
nReturned: 240,
keysExamined: 240,
docsExamined: 240,
ms: 0
}每个分支的索引都能按排序字段有序时,用 SORT_MERGE 归并,不需要 SORT 阶段;有一个分支不能就退化成 OR 之后再 SORT。
# 分支命中数、并集、交集
test> db.t.countDocuments({a: 5}) + " + " + db.t.countDocuments({b: 35}) + " (两个分支各自的命中数);并集 " + db.t.countDocuments({$or: [{a: 5}, {b: 35}]}) + ";同时满足两个分支的 " + db.t.countDocuments({a: 5, b: 35})
200 + 40 (两个分支各自的命中数);并集 200;同时满足两个分支的 40
test> db.t.find({$or: [{a: 5}, {b: 35}]})
{
plan: 'SUBPLAN > FETCH > OR[IXSCAN(a_1) | IXSCAN(b_1)]',
nReturned: 200,
keysExamined: 240,
docsExamined: 200,
ms: 0
}
test> db.t.find({$or: [{a: 5}, {a: 5, b: 3}]}) // 第二个分支被第一个包含,优化器直接化简
{
plan: 'FETCH > IXSCAN(a_1)',
nReturned: 200,
keysExamined: 200,
docsExamined: 200,
ms: 0
}两个分支命中同一个文档只会返回一次;键数是两边之和,FETCH 的文档数是并集。被另一个分支包含的分支会被化简掉。
test> db.t.find({$or: [{a: 5}, {a: 6}, {a: 7}]})
{
plan: 'FETCH > IXSCAN(a_1)',
nReturned: 600,
keysExamined: 601,
docsExamined: 600,
ms: 0
}
test> db.t.find({a: {$in: [5, 6, 7]}})
{
plan: 'FETCH > IXSCAN(a_1)',
nReturned: 600,
keysExamined: 601,
docsExamined: 600,
ms: 0
}
同一字段:$or 三个分支 0.9 ms;$in 0.9 ms
不同字段:一条 $or 查询 0.6 ms;应用端拆成两条查询再合并 0.7 ms同一个字段上的多个 $or 分支和 $in 的计划一样,耗时也一样。