MongoDB - 它如何避免完整的集合扫描?

时间:2014-03-09 00:37:53

标签: mongodb indexing

我有这个users集合,有1000000行 通过调用findOne,每个文档的结构如下所示 通过调用getIndexes也可以显示索引。所以我有 它上面有两个复合索引,只有它们的键的顺序不同。

此集合中的所有username值都是唯一的,
对于k = 0,1,2,...,999999,它们的形式为“user”+ k。

另外,我没有空白年龄或用户名。

[test] 2014-03-08 20:08:10.135 >>> db.users.aggregate({'$match':{ 'username':{'$exists':false} }}) ;
{ "result" : [ ], "ok" : 1 }
[test] 2014-03-08 20:08:27.760 >>> db.users.aggregate({'$match':{ 'age':{'$exists':false} }}) ;
{ "result" : [ ], "ok" : 1 }
[test] 2014-03-08 20:08:41.198 >>> db.users.find({username : null}).count();
0
[test] 2014-03-08 20:12:01.456 >>> db.users.find({age : null}).count();
0
[test] 2014-03-08 20:12:06.790 >>>

我在运行的explain中无法理解的是以下内容:
MongoDB如何只能扫描996291文档并避免扫描 剩余的3709份文件。 MongoDB如何确定他没有失踪 任何符合查询标准的文件(来自这些3709个)? 如果我们假设MongoDB仅使用
,我不明白这是怎么可能的 username_1_age_1索引。

C:\>C:\Programs\MongoDB\bin\mongo.exe
MongoDB shell version: 2.4.8
connecting to: test
Welcome to the MongoDB shell!
[test] 2014-03-08 19:31:41.683 >>> db.users.count();
1000000

[test] 2014-03-08 19:31:45.68 >>> db.users.findOne();
{
        "_id" : ObjectId("5318fac5e22bd6bc482baf88"),
        "i" : 0,
        "username" : "user0",
        "age" : 10,
        "created" : ISODate("2014-03-06T22:46:29.225Z")
}

[test] 2014-03-08 19:32:06.352 >>> db.users.getIndexes();
[
        {
                "v" : 1,
                "key" : {
                        "_id" : 1
                },
                "ns" : "test.users",
                "name" : "_id_"
        },
        {
                "v" : 1,
                "key" : {
                        "age" : 1,
                        "username" : 1
                },
                "ns" : "test.users",
                "name" : "age_1_username_1"
        },
        {
                "v" : 1,
                "key" : {
                        "username" : 1,
                        "age" : 1
                },
                "ns" : "test.users",
                "name" : "username_1_age_1"
        }
]

[test] 2014-03-08 19:31:49.941 >>> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).sort({"username" : 1}).hint({"username" : 1, "age" : 1}).explain();
{
        "cursor" : "BtreeCursor username_1_age_1",
        "isMultiKey" : false,
        "n" : 167006,
        "nscannedObjects" : 167006,
        "nscanned" : 996291,
        "nscannedObjectsAllPlans" : 167006,
        "nscannedAllPlans" : 996291,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 3,
        "nChunkSkips" : 0,
        "millis" : 3177,
        "indexBounds" : {
                "username" : [
                        [
                                {
                                        "$minElement" : 1
                                },
                                {
                                        "$maxElement" : 1
                                }
                        ]
                ],
                "age" : [
                        [
                                21,
                                30
                        ]
                ]
        },
        "server" : "mongo020:27017"
}
[test] 2014-03-08 19:32:06.352 >>>

更新 - 以下是如何重现的确切说明:

C:\>mongo

C:\>C:\Programs\MongoDB\bin\mongo.exe
MongoDB shell version: 2.4.8
connecting to: test
Welcome to the MongoDB shell!
[test] 2014-03-11 05:13:00.941 >>> function populate(){
...
... for (i=0; i<1000000; i++) {
...    db.users.insert({
...        "i" : i,
...        "username" : "user"+i,
...        "age" : Math.floor(Math.random()*60),
...         "created" : new Date()
...    }
...    );
... }
... }
[test] 2014-03-11 05:13:33.139 >>>
[test] 2014-03-11 05:15:46.689 >>> populate();
[test] 2014-03-11 05:16:46.366 >>> db.users.ensureIndex({username:1, age:1});
[test] 2014-03-11 05:17:05.476 >>>
[test] 2014-03-11 05:17:05.476 >>> db.users.count();
1000000
[test] 2014-03-11 05:18:35.297 >>> db.users.getIndexes();
[
        {
                "v" : 1,
                "key" : {
                        "_id" : 1
                },
                "ns" : "test.users",
                "name" : "_id_"
        },
        {
                "v" : 1,
                "key" : {
                        "username" : 1,
                        "age" : 1
                },
                "ns" : "test.users",
                "name" : "username_1_age_1"
        }
]
[test] 2014-03-11 05:19:54.657 >>>
[test] 2014-03-11 05:19:54.657 >>> db.users.find({"age" : {"$gte" : 21, "$lte" : 30}}).sort({"username" : 1}).hint({"username" : 1, "age" : 1}).explain();
{
        "cursor" : "BtreeCursor username_1_age_1",
        "isMultiKey" : false,
        "n" : 166799,
        "nscannedObjects" : 166799,
        "nscanned" : 996234,
        "nscannedObjectsAllPlans" : 166799,
        "nscannedAllPlans" : 996234,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 2,
        "nChunkSkips" : 0,
        "millis" : 2730,
        "indexBounds" : {
                "username" : [
                        [
                                {
                                        "$minElement" : 1
                                },
                                {
                                        "$maxElement" : 1
                                }
                        ]
                ],
                "age" : [
                        [
                                21,
                                30
                        ]
                ]
        },
        "server" : "mongo020:27017"
}
[test] 2014-03-11 05:20:44.15 >>>

3 个答案:

答案 0 :(得分:3)

我很确定这是由code这个位引起的2.4错误:

// If nscanned is increased by more than 20 before a matching key is found, abort
// skipping through the btree to find a matching key.  This iteration cutoff
// prevents unbounded internal iteration within BtreeCursor::init() and
// BtreeCursor::advance() (the callers of skipAndCheck()).  See SERVER-3448.
if ( _nscanned > startNscanned + 20 ) {
    skipUnusedKeys();
    // If iteration is aborted before a key matching _bounds is identified, the
    // cursor may be left pointing at a key that is not within bounds
    // (_bounds->matchesKey( currKey() ) may be false).  Set _boundsMustMatch to
    // false accordingly.
    _boundsMustMatch = false;
    return;
}

而且更加谨慎here

//don't include unused keys in nscanned
//++_nscanned;

当您扫描索引时,每次连续20次未命中时,您将失去增强的nscan。

您可以使用一个非常简单的示例进行复制:

> db.version()
2.4.8
>
> for (var i = 1; i<=100; i++){db.foodle.save({_id:i, name:'a'+i, age:1})}
> db.foodle.ensureIndex({name:1, age:1})
> db.foodle.find({ age:{ $gte:10, $lte:20 }}).hint({name:1, age:1}).explain()
{
        "cursor" : "BtreeCursor name_1_age_1",
        "isMultiKey" : false,
        "n" : 0,
        "nscannedObjects" : 0,
        "nscanned" : 96,
        "nscannedObjectsAllPlans" : 0,
        "nscannedAllPlans" : 96,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 1,
        "indexBounds" : {
                "name" : [
                        [
                                {
                                        "$minElement" : 1
                                },
                                {
                                        "$maxElement" : 1
                                }
                        ]
                ],
                "age" : [
                        [
                                10,
                                20
                        ]
                ]
        },
        "server" : "Jeffs-MacBook-Air.local:27017"
}

如果你改变了年龄,所以你没有得到20次失误,那么nscanned的值就是你所期望的:

for (var i = 1; i<=100; i++){
    var theAge = 1;
    if (i%10 == 0){ theAge = 15;}
    db.foodle.save({ _id:i, name:'a'+i, age: theAge });
}

{
        "cursor" : "BtreeCursor name_1_age_1",
        "isMultiKey" : false,
        "n" : 10,
        "nscannedObjects" : 10,
        "nscanned" : 100,
        "nscannedObjectsAllPlans" : 10,
        "nscannedAllPlans" : 100,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 0,
        "indexBounds" : {
                "name" : [
                        [
                                {
                                        "$minElement" : 1
                                },
                                {
                                        "$maxElement" : 1
                                }
                        ]
                ],
                "age" : [
                        [
                                10,
                                20
                        ]
                ]
        },
        "server" : "Jeffs-MacBook-Air.local:27017"
}

我不确定为什么增量被注释掉,但是这个代码在2.6中都已经改变了,应该返回你期望的nscanned。

答案 1 :(得分:1)

正确的解决方案&#34;不是强迫查询优化器使用与其符合条件的&#34;#34;的概念不匹配的索引。索引,但改为包括前导字段以及您正在约束的字段。这样做的好处是可以在 2.6 中使用索引而不使用(hacky)&#34;提示&#34; (如果您以后在{age:1,name:1}添加其他索引,可能会损害您的效果。

查询:

db.names.find({ name:{$lt:MaxKey ,$gt:MinKey}, age: {$gte: 21, $lte: 30}},
              {_id:0, age:1, name:1}).explain()

2.6解释:

{
    "cursor" : "BtreeCursor name_1_age_1",
    "isMultiKey" : false,
    "n" : 6010,
    "nscannedObjects" : 0,
    "nscanned" : 6012,
    "nscannedObjectsAllPlans" : 0,
    "nscannedAllPlans" : 6012,
    "scanAndOrder" : false,
    "indexOnly" : true,
    "nYields" : 46,
    "nChunkSkips" : 0,
    "millis" : 8,
    "indexBounds" : {
        "name" : [
            [
                {
                    "$minElement" : 1
                },
                {
                    "$maxElement" : 1
                }
            ]
        ],
        "age" : [
            [
                21,
                30
            ]
        ]
    },
    "server" : "Asyas-MacBook-Pro.local:27017",
    "filterSet" : false
}

2.4解释(你必须添加hint({name:1,age:1}).sort({name:1,age:1})来强制使用索引:

{
    "cursor" : "BtreeCursor name_1_age_1",
    "isMultiKey" : false,
    "n" : 6095,
    "nscannedObjects" : 0,
    "nscanned" : 6096,
    "nscannedObjectsAllPlans" : 103,
    "nscannedAllPlans" : 6199,
    "scanAndOrder" : false,
    "indexOnly" : true,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 10,
    "indexBounds" : {
        "name" : [
            [
                {
                    "$minElement" : 1
                },
                {
                    "$maxElement" : 1
                }
            ]
        ],
        "age" : [
            [
                21,
                30
            ]
        ]
    },
    "server" : "Asyas-MacBook-Pro.local:24800"
}

我添加了投影以显示&#34; indexOnly&#34;在两种情况下都是如此,如果删除投影,则计划相同但nscannedObjects与n相同而不是0。

答案 2 :(得分:0)

这真的是关于mongo&#34;放弃&#34;在它意识到可能的匹配已经用尽之后,将不再有匹配的项目。索引通过提供一些界限来帮助这里。

实际上这是解释它的部分:

"indexBounds" : {

          "age" : [
                    [
                            21,
                            30
                    ]
            ]

由于这是所选索引中的一个字段,因此mongo已设置开始的位置以及结束的位置。所以它只需要读取介于这些边界之间的文档。 这些文档的列表是索引的一部分。

以下是一些易于复制的代码:

people = [
    "Marc", "Bill", "George", "Eliot", "Matt", "Trey", "Tracy",
    "Greg", "Steve", "Kristina", "Katie", "Jeff"];

for (var i=0; i<200000; i++){
    name = people[Math.floor(Math.random()*people.length)];
    age = Math.floor(Math.random() * ( 50 - 18 + 1)) + 18;
    boolean = [true,false][Math.floor(Math.random()*2)];
    db.names.insert({ 
        name: name,
        age: age,
        boolean: boolean,
        added: new Date() 
    }); 
}

添加索引:

db.names.ensureIndex( { name: 1, age: 1 });

运行查询:

db.names.find({ 
    age: {$gte: 21, $lte: 30} 
}).hint( { name: 1, age: 1 } ).explain()

会得到类似的结果:

{
    "cursor" : "BtreeCursor name_1_age_1",
    "isMultiKey" : false,
    "n" : 60226,
    "nscannedObjects" : 60226,
    "nscanned" : 60250,
    "nscannedObjectsAllPlans" : 60226,
    "nscannedAllPlans" : 60250,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 227,
    "indexBounds" : {
            "name" : [
                    [
                            {
                                    "$minElement" : 1
                            },
                            {
                                    "$maxElement" : 1
                            }
                    ]
            ],
            "age" : [
                    [
                            21,
                            30
                    ]
            ]
    },
    "server" : "ubuntu:27017"
}

因此,您可以看到nscanned高于n但仍低于总文档数。这表明&#34;界限&#34;被考虑在内,当超出这些界限时,比赛将不再返回。

这里发生了什么?为什么返回的文件少于集合中的文件?基本上是问题的本质。

所以考虑一下。您知道您的复合索引指定首先匹配的字段。但是,不要将复合索引视为 join 语句(稍后会将其视为元素列表)。所以它确实在那里有age字段的离散值。

接下来,我们需要经过大量的文档数量。因此优化器会自然地讨厌进行扫描。但是,由于我们没有给出复合索引的第一个元素匹配或范围的条件,因此它必须开始这样做。所以我们开始一起开始。现在进行更直观的演示。

  

miss,miss,miss,hit,hit,&#34;很多点击&#34;,miss,miss,&#34;更多未命中&#34;,停止

为何停止。这是一种优化条件。由于我们有离散年龄值,确定所选索引中存在 bounds ,因此会询问问题。

  

&#34;等一下。我应该按顺序扫描这些,我只是有很多未命中。我想我错过了我的公共汽车站&#34;。

通俗地说,这是完全优化器的作用。并且当它超过它将找到更多匹配的点时实现它它会跳出公共汽车&#34;并带着结果走回家。所以比赛已经用尽了#34;超过合理确定将会有任何进一步匹配的点。

当然,如果字段的索引顺序被翻转,所以年龄是第一个或唯一的考虑因素,那么nscannedn将匹配,因为有明显清晰的起点和终点。 / p>

解释的目的是它可以解释分析查询语句时发生的事情。在这有它已经&#34;告诉&#34;您的查询条件询问以查询范围内的某个范围可以在索引中匹配,那么它将在扫描结果时使用该信息。

所以这里发生的事情是,考虑到用于搜索的索引的界限,优化器有一个&#34;的想法&#34;从哪里开始,然后到哪里结束。考虑到因素,一旦匹配&#34;不再看起来&#34;发现匹配已经用尽,而且过程放弃了#34;考虑到它不会找到任何其他的东西。

任何其他条件,例如你想知道你是否有没有用户名的文件将是无关紧要的,只有在索引是&#34;稀疏&#34;,然后它们根本不在索引中时才适用。这不是稀疏索引,也不存在空值。但这并不是理解为什么查询没有通过所有文档的重要部分。

你可能正在努力的是这是一个复合指数。但这不像是一个关于&#34;连接&#34;的索引。条款,因此索引必须扫描username + age。相反,两个字段都可以考虑,只要它们可以在&#34; order&#34;中考虑。这就是为什么解释输出显示它已匹配这些边界。

documentation并非如此明确。但确定了indexBounds的含义。


修改

最后的陈述是,这是已确认和预期的行为,以及声明的&#34; Bug&#34;实际上并不是一个bug,而是2.6版本中引入的一个bug,它包含了Index接口代码的主要重要因素。请参阅我报告的SERVER-13197

因此,通过改变查询,可以在2.6中实现与所示相同的结果:

 db.names.find({
      "name": { "$gt": MinKey, "$lt": MaxKey },
      "age": {$gte: 21, $lte: 30}  
 }).sort( { "name": 1, "age": 1 } ).explain()

{
    "cursor" : "BtreeCursor name_1_age_1",
    "isMultiKey" : false,
    "n" : 60770,
    "nscannedObjects" : 60770,
    "nscanned" : 60794,
    "nscannedObjectsAllPlans" : 60770,
    "nscannedAllPlans" : 60794,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 474,
    "nChunkSkips" : 0,
    "millis" : 133,
    "indexBounds" : {
            "name" : [
                    [
                            {
                                    "$minElement" : 1
                            },
                            {
                                    "$maxElement" : 1
                            }
                    ]
            ],
            "age" : [
                    [
                            21,
                            30
                    ]
            ]
    },
    "server" : "ubuntu:27017",
    "filterSet" : false
}

这表明通过在第一个索引元素上包含MinKeyMaxKey值,优化器会正确检测到第二个元素上的边界可以按照已经描述的方式使用

当然,在早期版本中不需要这样,因为使用sort足以指定此索引,并且优化器可以正确检测边界而无需对查询进行显式修改。

正如此问题所述,此修复程序旨在在将来的版本中发布。