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

标签 ruby-on-rails regex mongodb

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

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

Customer 是 Order 文档中的嵌入文档,并且对客户的电子邮件进行了索引。

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

       user     system      total        real
   0.090000   0.010000   0.100000 ( 27.656723)

我想也许是 \A\z 导致了速度变慢,所以我尝试了以下方法来查找以给定参数开头的电子邮件:Order .where("customer.email"=>/^test/i).count

结果并没有太大不同。

       user     system      total        real
   0.090000   0.010000   0.100000 ( 28.712883)

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

       user     system      total        real
   0.080000   0.000000   0.080000 (  0.122888)

当我查看 explain 的输出时,它显示使用 regexp 扫描所有文档。

{
                     "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
        ]
    ]
  }
}

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

{
                     "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"
        ]
    ]
  }
}

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

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

最佳答案

快速版:

这里有一个不区分大小写的正则表达式(/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"})

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

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"
}

您会看到它只需要扫描一个文档,并且扫描的上限和下限是明确定义的(“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"
}

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

关于ruby-on-rails - 为什么在 Mongodb 查询中使用正则表达式会导致它扫描所有文档而不是索引?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29262658/

相关文章:

mysql - rails 显示sql结果如下

ruby-on-rails - 可以 Rails 应用程序和 rake db :migrate use different database credentials?

ruby-on-rails - underscore在ruby中的pick方法

ruby-on-rails - 我什么时候应该使用 Lambda 或匿名方法?

r - 如何在 R 的字符类中包含右方括号?

写关注中的 MongoDb 日志

php - 正则表达式拆分 TitleCase Word

regex - 在 PowerShell 中按列拆分文本

mongodb - Mongo - 更新嵌套数组中的元素

python - Mongoengine链接filter()和ReferenceField()导致 "TypeError: ' Collection'对象不可调用”