为什么在 Mongodb 查询中使用正则表达式会导致它扫描所有文档而不是索引?

yan*_*man 2 regex ruby-on-rails mongodb

当我试图调查查询的性能问题时,我发现源是这个查询。我正在使用带有 Mongoid gem 的 Rails 4。

Order.where("customer.email" => /\Atest@email.com\z/i)哪里test@email.com只是一个例子。

客户是订单文档中的嵌入文档,客户的电子邮件被索引。

当我使用Benchmark.bmbmwhereOrder.where("customer.email" => /\Atest@email.com\z/i).count重复100次数对性能进行基准测试时,我得到了以下结果。

       user     system      total        real
   0.090000   0.010000   0.100000 ( 27.656723)
Run Code Online (Sandbox Code Playgroud)

我认为可能\A并且\z导致了缓慢,所以我尝试了以下方法,它查找以给定参数开头的电子邮件:Order.where("customer.email" => /^test/i).count

结果并没有太大的不同。

       user     system      total        real
   0.090000   0.010000   0.100000 ( 28.712883)
Run Code Online (Sandbox Code Playgroud)

作为最后的手段,我尝试只匹配整个字符串而不使用正则表达式。这一次,它产生了巨大的变化:Order.where("customer.email" => "test@email.com").count

       user     system      total        real
   0.080000   0.000000   0.080000 (  0.122888)
Run Code Online (Sandbox Code Playgroud)

当我查看解释的输出时,它表明使用正则表达式扫描所有文档。

{
                     "cursor" => "BtreeCursor customer.email_1",
                 "isMultiKey" => false,
                          "n" => 781,
            "nscannedObjects" => 781,
                   "nscanned" => 500000,
    "nscannedObjectsAllPlans" => 781,
           "nscannedAllPlans" => 500000,
               "scanAndOrder" => false,
                  "indexOnly" => false,
                    "nYields" => 1397,
                "nChunkSkips" => 0,
                     "millis" => 406,
            "indexBounds" => {
    "customer.email" => [
        [0] [
            [0] "",
            [1] {}
        ],
        [1] [
            [0] /test/i,
            [1] /test/i
        ]
    ]
  }
}
Run Code Online (Sandbox Code Playgroud)

虽然使用整个字符串只扫描子集,这是我所期望的。

{
                     "cursor" => "BtreeCursor customer.email_1",
                 "isMultiKey" => false,
                          "n" => 230,
            "nscannedObjects" => 230,
                   "nscanned" => 230,
    "nscannedObjectsAllPlans" => 230,
           "nscannedAllPlans" => 230,
               "scanAndOrder" => false,
                  "indexOnly" => false,
                    "nYields" => 1,
                "nChunkSkips" => 0,
                     "millis" => 0,
            "indexBounds" => {
    "customer.email" => [
        [0] [
            [0] "test@email.com",
            [1] "test@email.com"
        ]
    ]
  }
}
Run Code Online (Sandbox Code Playgroud)

有人可以向我解释为什么在 mongodb 查询中使用正则表达式会导致它扫描所有文档而不是索引吗?

编辑:在解释输出中添加了 indexBounds,在原始帖子中被省略了。

Chr*_*ald 5

快速版本:

这里有一个不区分大小写的正则表达式(/i标志);这意味着 Mongo 不能对索引进行前缀匹配,因此必须扫描整个索引(因为它不知道您是否想要 test@example.com 或 TEST@example.com 或 teST@exAMple.com 或什么的)。

如果您想在 Mongo 中进行不区分大小写的查找,正确的解决方案是在存储之前将它们小写。如果您需要不影响用户输入的输入,则将其存储在文档的次要字段中(即 email 和 email_normalized)。

更长的版本

Mongo 的索引是 B 树,当您执行正则表达式查询时,它会看到 a) 是否可以使用索引(按字段)和 b) 它必须扫描多少棵树才能确保结果。在您有特定前缀的情况下,Mongo 知道它可以将搜索限制在树的一部分。您遗漏了解释中最重要的部分 - 索引边界。给定一个带有索引和一些电子邮件的集合:

kerrigan:PRIMARY> db.test.ensureIndex({email: 1})
kerrigan:PRIMARY> db.test.insert({email: "test@example.com"})
kerrigan:PRIMARY> db.test.insert({email: "teTE@example.com"})
kerrigan:PRIMARY> db.test.insert({email: "teST@example.com"})
kerrigan:PRIMARY> db.test.insert({email: "TEst@example.com"})
Run Code Online (Sandbox Code Playgroud)

如果我们在不区分大小写的匹配项上解释查找:

kerrigan:PRIMARY> db.test.find({email: /\Atest@example.com\z/}).explain()
{
        "cursor" : "IndexCursor email_1 multi",
        "isMultiKey" : false,
        "n" : 1,
        "nscannedObjects" : 1,
        "nscanned" : 1,
        "nscannedObjectsAllPlans" : 1,
        "nscannedAllPlans" : 1,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nChunkSkips" : 0,
        "millis" : 0,
        "indexBounds" : {
                "email" : [
                        [
                                "test@example",
                                "test@examplf"
                        ],
                        [
                                /\Atest@example.com\z/,
                                /\Atest@example.com\z/
                        ]
                ]
        },
        "server" : "luna:27019"
}
Run Code Online (Sandbox Code Playgroud)

您会看到它只需要扫描一个文档,并且扫描的上限和下限是明确定义的(“test@example”..“test@examplf”)。这是因为 Mongo 查看前缀并说“该显式前缀保证在每个匹配结果中”,因此知道它可以限制它必须扫描的索引部分。

如果我们添加 /i 标志:

kerrigan:PRIMARY> db.test.find({email: /\Atest@example.com\z/i}).explain()
{
        "cursor" : "IndexCursor email_1 multi",
        "isMultiKey" : false,
        "n" : 3,
        "nscannedObjects" : 3,
        "nscanned" : 4,
        "nscannedObjectsAllPlans" : 3,
        "nscannedAllPlans" : 4,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nChunkSkips" : 0,
        "millis" : 0,
        "indexBounds" : {
                "email" : [
                        [
                                "",
                                {

                                }
                        ],
                        [
                                /\Atest@example.com\z/i,
                                /\Atest@example.com\z/i
                        ]
                ]
        },
        "server" : "luna:27019"
}
Run Code Online (Sandbox Code Playgroud)

突然那些索引边界变成了“”..“”,或者是全索引扫描;因为该字段没有保证的静态前缀,所以 Mongo 必须扫描并检查索引中的每个值,以查看它是否与您提供的正则表达式匹配。