什么样的正则能用上前缀索引?/i 为什么慢?这篇用 30 万个词和 explain 的 indexBounds 逐个验证,并给出大小写不敏感前缀查询的办法。
/^abc/: 16 个 /^abc$/: 1 个 /abc/: 123 个 /^abc.*/: 16 个 /^(abc|abd)/: 27 个 /^abc/i: 23 个
简单的 /^abc/ 被收窄成字符串范围 ["abc", "abd"),只检查命中的少数键;/^abc.*/ 同样。
# 锚定前缀
test> db.t.find({name: /^abc/})
{
plan: 'FETCH > IXSCAN(name_1)',
nReturned: 16,
keysExamined: 17,
docsExamined: 16,
ms: 1
}
# 不锚定
test> db.t.find({name: /abc/})
{
plan: 'FETCH > IXSCAN(name_1)',
nReturned: 123,
keysExamined: 300006,
docsExamined: 123,
ms: 111
}
# 不区分大小写
test> db.t.find({name: /^abc/i})
{
plan: 'FETCH > IXSCAN(name_1)',
nReturned: 23,
keysExamined: 300006,
docsExamined: 23,
ms: 104
}
# ^ 后面是 .*,等于不锚定
test> db.t.find({name: /^.*abc/})
{
plan: 'FETCH > IXSCAN(name_1)',
nReturned: 123,
keysExamined: 300006,
docsExamined: 123,
ms: 118
}
# 前缀后面跟 .*
test> db.t.find({name: /^abc.*/})
{
plan: 'FETCH > IXSCAN(name_1)',
nReturned: 16,
keysExamined: 17,
docsExamined: 16,
ms: 0
}不锚定的 /abc/、不区分大小写的 /^abc/i、/^.*abc/、带分支的 /^(abc|abd)/ 的范围都是整个索引 ["", {}),在每个键上匹配正则;只有命中的文档才会 FETCH。
test> /^abc/ 的 indexBounds
{"name":["[\"abc\", \"abd\")","[/^abc/, /^abc/]"]}
test> /^abc$/ 的 indexBounds
{"name":["[\"abc\", \"abd\")","[/^abc$/, /^abc$/]"]}
test> /^(abc|abd)/ 的 indexBounds
{"name":["[\"\", {})","[/^(abc|abd)/, /^(abc|abd)/]"]}
test> /abc/ 的 indexBounds
{"name":["[\"\", {})","[/abc/, /abc/]"]}
test> /^abc/i 的 indexBounds
{"name":["[\"\", {})","[/^abc/i, /^abc/i]"]}explain 的 indexBounds 里能直接看出范围有没有被收窄成字符串区间。
# name_lc 是 name 的小写副本
test> db.t.find({name_lc: /^abc/})
{
plan: 'FETCH > IXSCAN(name_lc_1)',
nReturned: 23,
keysExamined: 24,
docsExamined: 23,
ms: 3
}
命中 23 个(= /^abc/i 的 23 个)
# ci 集合的索引带 collation
test> db.ci.find({name: /^abc/i}).collation({locale: "en", strength: 2})
{
plan: 'COLLSCAN',
nReturned: 8,
keysExamined: 0,
docsExamined: 100000,
ms: 30
}另存一个小写副本字段并建索引,用小写前缀查询,只检查 24 个键;正则不使用 collation,带 collation 的索引帮不上忙。
/^abc/ 0.2 ms
/^abc$/(等值) 0.2 ms
/abc/(不锚定) 100 ms
/^abc/i 98 ms
name_lc /^abc/ 0.3 ms
{name: "abc"} 等值 0.2 ms能收窄的写法 0.2~0.3 ms,扫整个索引的写法约 100 ms。
test> db.t.find({name: {$in: [/^abcd/, /^abce/]}})
{
plan: 'FETCH > IXSCAN(name_1)',
nReturned: 2,
keysExamined: 3,
docsExamined: 2,
ms: 0
}
test> /^(abcd|abce)/ 的 indexBounds
{"name":["[\"\", {})","[/^(abcd|abce)/, /^(abcd|abce)/]"]}把多个前缀分开写进 $in,每个都是前缀范围(检查 3 个键);写成一个带分支的正则就退化成整个索引。