【问题标题】:Why is this $elemMatch query not using my index?为什么这个 $elemMatch 查询不使用我的索引?
【发布时间】:2014-08-19 21:19:18
【问题描述】:

我的查询:

{
    "unique_contact_method.enrichments": {
        "$not": {
            "$elemMatch": {
                "created_by.name": "fullcontact"
            }
        }
    }
}

我的索引:

{
    v: 1,
    name: "unique_contact_method.enrichments.created_by.name_1",
    key: {
        "unique_contact_method.enrichments.created_by.name": 1
    },
    ns: "app27434806.unique_contact_methods",
    background: true,
    safe: true
}

.explain() 结果:

为什么没有索引?

【问题讨论】:

  • 请添加示例文档。

标签: mongodb mongodb-query mongodb-indexes


【解决方案1】:

这里使用$not 操作符是无法使用索引的原因。文档中有一个声明“暗示”了这一点,如果不是完全清楚的话:

"记住$not操作符只影响其他操作符,不能独立检查字段和文档。所以,使用$not操作符进行逻辑析取,使用$ne操作符直接测试字段的内容。"

其中的基本短语是“无法检查字段”,这意味着它实际上并没有像使用索引那样“测试”字段的值。一个简单的文档最好地解释了这一点:

{ 
    "_id" : ObjectId("53f3e414deee3a78e47e57e2"), 
    "created" : [ { "name" : "Bill" }, { "name" : "Ted" } ]
}

当然,索引是在“created.name”上创建的。

现在考虑以下查询并解释输出:

db.doctest.find({ "created": { "$elemMatch": { "name": "Bill" } } }).explain()

{
    "cursor" : "BtreeCursor created.name_1",
    "isMultiKey" : true,
    "n" : 1,
    "nscannedObjects" : 1,
    "nscanned" : 1,
    "nscannedObjectsAllPlans" : 1,
    "nscannedAllPlans" : 1,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 0,
    "indexBounds" : {
            "created.name" : [
                    [
                            "Bill",
                            "Bill"
                    ]
            ]
    },
    "server" : "ubuntu:27017",
    "filterSet" : false
}

这只是选择索引并按预期显示索引边界。

不要用$not看这个,我要用.hint()“强制”索引:

db.doctest.find({ "created": { "$not": { "$elemMatch": { "name": "Bill" } } } }).hint({ "created.name": 1 }).explain()
{
    "cursor" : "BtreeCursor created.name_1",
    "isMultiKey" : true,
    "n" : 0,
    "nscannedObjects" : 1,
    "nscanned" : 2,
    "nscannedObjectsAllPlans" : 1,
    "nscannedAllPlans" : 2,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 0,
    "indexBounds" : {
            "created.name" : [
                    [
                            {
                                    "$minElement" : 1
                            },
                            {
                                    "$maxElement" : 1
                            }
                    ]
            ]
    },
    "server" : "ubuntu:27017",
    "filterSet" : false
}

这里要看的重要部分是“indexBounds”。这解释了为什么没有提示就不会使用索引,因为简单地说,没有“界限”可供选择。 $not 操作基本上说:

“查看条件测试的每个值,如果为真,则认为它为假,或者基本上相反”

这里的最终评估是“Ted”不是“Bill”,因此条件为真,但没有办法使用索引“查找”。

所以这里的考虑是你如何做同样的事情并使用索引?文档中的段落告诉您,为了考虑“字段”,您需要改用 $ne 运算符:

db.doctest.find({ "created": { "$elemMatch": { "name": { "$ne": "Bill" } } } }).explain()
{
    "cursor" : "BtreeCursor created.name_1",
    "isMultiKey" : true,
    "n" : 1,
    "nscannedObjects" : 1,
    "nscanned" : 2,
    "nscannedObjectsAllPlans" : 1,
    "nscannedAllPlans" : 2,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 0,
    "indexBounds" : {
            "created.name" : [
                    [
                            {
                                    "$minElement" : 1
                            },
                            "Bill"
                    ],
                    [
                            "Bill",
                            {
                                    "$maxElement" : 1
                            }
                    ]
            ]
    },
    "server" : "ubuntu:27017",
    "filterSet" : false
}

现在,“indexBounds”向您展示了索引本质上是用于“过滤掉”所提供的值。因此,该索引用于提取除“Bill”之外的任何其他值。

这里的结论是$not 有它的逻辑用途,但在许多情况下你真正想要的是$ne。在必须应用$not 的情况下,请注意不会使用字段值的索引来进行比较。

【讨论】:

  • db.doctest.find({ "created": { "$not": { "$elemMatch": { "name": "Bill" } } } })db.doctest.find({ "created": { "$elemMatch": { "name": { "$ne": "Bill" } } } }) 不同。第一个返回 true,只有每个元素的名称不是“Bill”,如果有一个元素的名称不是“Bill”,第二个返回 true。我认为 $not 只是做逻辑反转,而且只做一次。
  • @Wizard 是的,但这里的重点是演示“为什么”没有使用索引以及可以使用索引和不能使用索引的不同排列。
  • 我也有 Jordan Feldstein 的困惑。由于 $not 具有严格的语法 { field: { $not: { <operator-expression> } } } - 我猜它明确地作为 NOT { field: <operator-expression> } 工作,那么为什么 mongodb 不对其进行优化以使其能够使用索引?正如你所说,也许排列使它不可能。非常感谢。
  • @NeilLunn:回答者中的冠军。
【解决方案2】:

有时我发现索引已自动用于查询,即使操作员$not 加入了操作。它让我想起 这个问题也让我困惑了很久。我尝试了新的线索并发现了一些不同的东西。我想我终于找到了答案。欢迎大家在这里发表评论,如果发现其他不同之处。

在 mongo shell V2.6.4 上运行

初始化数据如下:

> db.a.drop();  
false

> db.a.insert({_id:1, a:[1,2,3], b:[{x:1, y:2}, {x:4, y:4}], c:1});
WriteResult({ "nInserted" : 1 })
> db.a.insert({_id:2, a:[4,2,3], b:[{x:1, y:2}, {x:4, y:4}], c:1});
WriteResult({ "nInserted" : 1 })

> db.a.ensureIndex({a:1}, {name:"a"});
{
        "createdCollectionAutomatically" : false,
        "numIndexesBefore" : 1,
        "numIndexesAfter" : 2,
        "ok" : 1
}
> db.a.ensureIndex({"b.x":1}, {name:"bx"});
{
        "createdCollectionAutomatically" : false,
        "numIndexesBefore" : 2,
        "numIndexesAfter" : 3,
        "ok" : 1
}
> db.a.ensureIndex({c:1}, {name:"c"});
{
        "createdCollectionAutomatically" : false,
        "numIndexesBefore" : 3,
        "numIndexesAfter" : 4,
        "ok" : 1
}
> db.a.getIndexes();
[
        {
                "v" : 1,
                "key" : {
                        "_id" : 1
                },
                "name" : "_id_",
                "ns" : "test.a"
        },
        {
                "v" : 1,
                "key" : {
                        "a" : 1
                },
                "name" : "a",
                "ns" : "test.a"
        },
        {
                "v" : 1,
                "key" : {
                        "b.x" : 1
                },
                "name" : "bx",
                "ns" : "test.a"
        },
        {
                "v" : 1,
                "key" : {
                        "c" : 1
                },
                "name" : "c",
                "ns" : "test.a"
        }
]

> db.a.find();
{ "_id" : 1, "a" : [ 1, 2, 3 ], "b" : [ { "x" : 1, "y" : 2 }, { "x" : 2, "y" : 3 } ], "c" : 1 }
{ "_id" : 2, "a" : [ 4, 2, 3 ], "b" : [ { "x" : 1, "y" : 2 }, { "x" : 4, "y" : 4 } ], "c" : 1 }

此块只是简单地证明即使$not 加入查询操作,索引也会自动正确使用。

> db.a.find({c:{$not:{$gte:1}}}).explain();
{
        "cursor" : "BtreeCursor c",
        "isMultiKey" : false,
        "n" : 0,
        "nscannedObjects" : 0,
        "nscanned" : 1,
        "nscannedObjectsAllPlans" : 0,
        "nscannedAllPlans" : 1,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 0,
        "indexBounds" : {
                "c" : [
                        [
                                {
                                        "$minElement" : 1
                                },
                                1
                        ],
                        [
                                Infinity,
                                {
                                        "$maxElement" : 1
                                }
                        ]
                ]
        },
        "server" : "Duke-PC:27017",
        "filterSet" : false
}

这是原问题提到的风格。索引已自动使用。

> db.a.find({b:{$elemMatch:{x:{$gte:1}}}}).explain();
{
        "cursor" : "BtreeCursor bx",            // attention on this line
        "isMultiKey" : true,
        "n" : 2,
        "nscannedObjects" : 2,
        "nscanned" : 4,
        "nscannedObjectsAllPlans" : 2,
        "nscannedAllPlans" : 4,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 9,
        "indexBounds" : {
                "b.x" : [
                        [
                                1,
                                Infinity
                        ]
                ]
        },
        "server" : "Duke-PC:27017",
        "filterSet" : false
}

$elemMatch 之前使用运算符 $not 时,索引不起作用。这是这个问题的核心。

> db.a.find({b:{$not:{$elemMatch:{x:{$gte:1}}}}}).explain();
{
        "cursor" : "BasicCursor",           // attention on this line
        "isMultiKey" : false,
        "n" : 0,
        "nscannedObjects" : 2,
        "nscanned" : 2,
        "nscannedObjectsAllPlans" : 2,
        "nscannedAllPlans" : 2,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 0,
        "server" : "Duke-PC:27017",
        "filterSet" : false
}

这个块:找到一些方法来解释数组字段索引的机制。
共有两个文件,但nscanned: 6。这告诉我们索引是如何根据数组类型构建的。也就是说,索引节点位于数组的每个元素上,而不是数组本身。我想象字段a 上的索引结构是这样的:
BTree: Node(value:1, entry:[entry({_id:1})]), Node(value:2, entry:[entry({_id:1}), entry({_id:2})]), ...
当然,这只是我的想象以作解释。 :)

> db.a.find({a:{$gte:1}}).explain();
{
        "cursor" : "BtreeCursor a",
        "isMultiKey" : true,
        "n" : 2,
        "nscannedObjects" : 2,
        "nscanned" : 6,                 // attention on this line
        "nscannedObjectsAllPlans" : 2,
        "nscannedAllPlans" : 6,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 0,
        "indexBounds" : {
                "a" : [
                        [
                                1,
                                Infinity
                        ]
                ]
        },
        "server" : "Duke-PC:27017",
        "filterSet" : false
}

使用运算符$not 时,自动采用相关索引。而“indexBounds”字段告诉我们$not如何处理查询。

> db.a.find({a:{$not:{$gte:2}}},{_id:0,a:1}).explain();
{
        "cursor" : "BtreeCursor a",
        "isMultiKey" : true,
        "n" : 0,
        "nscannedObjects" : 1,          // attention on this field
        "nscanned" : 2,                 // attention on this field
        "nscannedObjectsAllPlans" : 1,
        "nscannedAllPlans" : 2,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 0,
        "indexBounds" : {               // attention on this field
                "a" : [
                        [
                                {
                                        "$minElement" : 1
                                },
                                2
                        ],
                        [
                                Infinity,
                                {
                                        "$maxElement" : 1
                                }
                        ]
                ]
        },
        "server" : "Duke-PC:27017",
        "filterSet" : false
}

插入具有相同字段名称a 但不是数组的新文档。

> db.a.insert({a:1});
WriteResult({ "nInserted" : 1 })
> db.a.find();
{ "_id" : 1, "a" : [ 1, 2, 3 ], "b" : [ { "x" : 1, "y" : 2 }, { "x" : 2, "y" : 3 } ], "c" : 1 }
{ "_id" : 2, "a" : [ 4, 2, 3 ], "b" : [ { "x" : 1, "y" : 2 }, { "x" : 4, "y" : 4 } ], "c" : 1 }
{ "_id" : ObjectId("541e4fcbb65042180c128280"), "a" : 1 }

请阅读此块与上面的内容进行比较。

> db.a.find({a:{$not:{$gte:2}}},{_id:0,a:1}).explain();
{
        "cursor" : "BtreeCursor a",
        "isMultiKey" : true,        // This tells engine there are repeated array elements on index.
        "n" : 1,
        "nscannedObjects" : 2,      // The third document should only access the index to fetch data 
                                    // since it has enough information.
                                    // But here engine still read from the collection. My unstanding is the engine 
                                    // can not distinguish whether this index field is an array element or not, 
                                    // so it has to access the collection to find more information.
        "nscanned" : 3,
        "nscannedObjectsAllPlans" : 2,
        "nscannedAllPlans" : 3,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 25,
        "indexBounds" : {
                "a" : [
                        [
                                {
                                        "$minElement" : 1
                                },
                                2
                        ],
                        [
                                Infinity,
                                {
                                        "$maxElement" : 1
                                }
                        ]
                ]
        },
        "server" : "Duke-PC:27017",
        "filterSet" : false
}

结论:

  1. elemMatch很特别:
    • $elemMatch 明确告诉字段“b”是一个数组。
    • 并且根据此运算符的查询定义,找到与查询匹配的任何元素然后true 可以立即返回。但只要完成对数组所有元素的扫描,没有找到任何满意的元素,则可以返回false
    • But index structure (think about my imagination above) on array can not support this kind of operation because engine can not determine which nodes on index are exactly from a certain array, if only by index. This is the most important point to explain this question.
  2. 其他算子在自己的查询定义中没有这个限制,比如$gte, $lt, ...,因为只有一个匹配才能判断是否匹配,可以直接通过索引来满足。

最后,有一种方法可以解决原始问题,但并不完美,因为必须提供整个元素。
数组字段上的索引,而不是元素。

> db.a.ensureIndex({b:1});
{
        "createdCollectionAutomatically" : false,
        "numIndexesBefore" : 4,
        "numIndexesAfter" : 5,
        "ok" : 1
}
> db.a.find({b:{$ne:{x:2, y:3}}}).explain();
{
        "cursor" : "BtreeCursor b_1",
        "isMultiKey" : true,
        "n" : 1,
        "nscannedObjects" : 2,
        "nscanned" : 4,
        "nscannedObjectsAllPlans" : 2,
        "nscannedAllPlans" : 4,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 33,
        "indexBounds" : {
                "b" : [
                        [
                                {
                                        "$minElement" : 1
                                },
                                {
                                        "x" : 2,
                                        "y" : 3
                                }
                        ],
                        [
                                {
                                        "x" : 2,
                                        "y" : 3
                                },
                                {
                                        "$maxElement" : 1
                                }
                        ]
                ]
        },
        "server" : "Duke-PC:27017",
        "filterSet" : false
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-05
    • 1970-01-01
    • 1970-01-01
    • 2011-08-24
    • 2021-12-17
    相关资源
    最近更新 更多