如何避免mongo shell 查询扫描文档

问题来源

Mongodb数据库的查询基于索引的匹配,当一个查询事件没有匹配索引的时候就会扫描整个collection的所有文档,导致效率非常低下,如下:

测试使用的一个用户表,5161条数据,可以看到有Pid的索引,没有login_account的索引

>db.persons.getIndexes()
[
{
    "v" : 1,
    "key" : {
        "_id" : 1
    },
    "name" : "_id_",
    "ns" : "roster.persons"
},
{
    "v" : 1,
    "unique" : true,
    "key" : {
        "did" : 1,
        "pid" : 1
    },
    "name" : "did_1_pid_1",
    "ns" : "roster.persons",
    "background" : true
},
{
    "v" : 1,
    "key" : {
        "pid" : 1
    },
    "name" : "pid_1",
    "ns" : "roster.persons",
    "background" : true
}
]

我们这里用匹配索引的字段和不匹配索引的字段做查询,并执行查询分析,限于篇幅我删掉了其他跟本文无关的输出,也不过多的讲解mongdb的查询分析:

>db.persons.find({pid:1774},{did:1,pid:1,name:1,login_account:1})
{ "id" : ObjectId("55e6c83583cdaea2c239db8b"), "pid" : NumberLong(1774), "name" : "llh安卓马甲", "login_account" : "23728632463", "did" : NumberLong(10000) }
>db.persons.find({login_account:"23728632463"},{did:1,pid:1,name:1,login_account:1})
{ "id" : ObjectId("55e6c83583cdaea2c239db8b"), "pid" : NumberLong(1774), "name" : "llh安卓马甲", "login_account" : "23728632463", "did" : NumberLong(10000) }
>db.persons.find({login_account:23728632463},{did:1,pid:1,name:1,login_account:1}).explain('allPlansExecution')
{

..........

"executionStats" : {
    "executionSuccess" : true,
    "nReturned" : 0,
    "executionTimeMillis" : 8,
    "totalKeysExamined" : 0,
    "totalDocsExamined" : 5161,

    ...........

},

...............

}
>db.persons.find({pid:1774},{did:1,pid:1,name:1,login_account:1}).explain('allPlansExecution')
{
................
"executionStats" : {
    "executionSuccess" : true,
    "nReturned" : 1,
    "executionTimeMillis" : 0,
    "totalKeysExamined" : 1,
    "totalDocsExamined" : 1,
    ..............
},
..............
}

可以明显看出匹配索引的查询很快处理,只需要读一个文档,但是没有匹配索引的会扫描整个表得所有文档,虽然这里看到时间相差不大,但当一个表达到上亿的时候,一次非匹配索引的查询会让整个数据库卡死,线上数据库做一些异常数据查询的操作时候,还可能导致业务雪崩。可能会说都匹配索引的时候就好了?但是谁能保证每次都敲的命令没有问题呢,比如上面查询pid的时候,我敲错了一个字母变成了aid:

>db.persons.find({aid:1774},{did:1,pid:1,name:1,login_account:1}).explain('allPlansExecution')
{
......
"executionStats" : {
    "executionSuccess" : true,
    "nReturned" : 0,
    "executionTimeMillis" : 8,
    "totalKeysExamined" : 0,
    "totalDocsExamined" : 5161,
    .......
},
.......
}

也会变成扫描整个表,导致性能急剧下降,另外这个扫描事件不太好中断(ctrl+c是无法结束的,mongo数据库还是会继续执行这条命令,具体的结束方案另外再论)。

解决方法

mongo查询是通过一个mongo的客户端工具,这个工具是开源的,修改代码在查询的时候发现没有匹配索引的查询拒绝查询,具体的代码请先自行GitHub上面下载,下面以mongo-3.2版本的代码为例子修改。

代码路劲 mongo-3.2/srv/shell/collection.js,shell是客户端工具的代码路劲,collection.js是表的操作合集,find,findOne,count等都是通过find查询,所以我们在find函数这里加上一个对查询条件匹配索引的校验,具体代码如下:

DBCollection.prototype.__checkIndexesMatch = function(query) {

    if (query == null) { //没有查询条件的,建立迭代器,不需要校验
        return true;
    }
    
    var count = this.count(null, null);
    if (count < 1000) {// 如果表里面的数据太少,不做校验
        return true;
    }
    
    var indexes = this.getIndexes(null, null); //获取所有的索引
        var maxMatch = 0; 
        //遍历索引,看是否都是否查询条件里面是不是能匹配上索引
        for (var index in indexes) {  
            var Match = 0;
            for (var i in indexes[index].key) {
                if (query.hasOwnProperty(i)) {
                    Match++;
                } else {
                    break;
                }
            }
            if (maxMatch < Match) {
                maxMatch = Match;
            }
        }
        // 最少要匹配一个索引字段,否则不予查询,这里索引匹配度可以自行定义
        if (maxMatch > 0) {
            return true;
        }
        return false;
    }

DBCollection.prototype.find = function(query, fields, limit, skip, batchSize, options) {
    if (this.__checkIndexesMatch(query) == false) {
        throw Error("Find not match indexes ! Please use original mongo!!!");
    }

    var cursor = new DBQuery(this._mongo,
                             this._db,
                             this,
                             this._fullName,
                             this._massageObject(query),
                             fields,
                             limit,
                             skip,
                             batchSize,
                             options || this.getQueryOptions());
    
    var connObj = this.getMongo();
    var readPrefMode = connObj.getReadPrefMode();
    if (readPrefMode != null) {
        cursor.readPref(readPrefMode, connObj.getReadPrefTagSet());
    }
    
    return cursor;

};

其中__checkIndexesMatch()是新增的函数,修改好后编译成mongo客户端(具体的编译方法,本文不做描述,可以参考),替换后我们再次执行异常查询效果如下:

> db.persons.find({aid:1774},{did:1,pid:1,name:1,login_account:1})
2019-03-14T20:16:12.410+0800 E QUERY    [thread1] Error: Find not match indexes ! Please use original mongo!!! :
DBCollection.prototype.find@src/mongo/shell/collection.js:301:1
@(shell):1:1

> db.persons.find({login_account:"23728632463"},{did:1,pid:1,name:1,login_account:1})
2019-03-14T20:16:30.836+0800 E QUERY    [thread1] Error: Find not match indexes ! Please use original mongo!!! :
DBCollection.prototype.find@src/mongo/shell/collection.js:301:1
@(shell):1:1

> db.persons.find({pid:1774},{did:1,pid:1,name:1,login_account:1})
{ "_id" : ObjectId("55e6c83583cdaea2c239db8b"), "pid" : NumberLong(1774), "name" : "llh安卓马甲", "login_account" : "23728632463", "did" : NumberLong(10000) }

可以看到前俩个不匹配索引导致低效的查询会被拒绝,从而达到线上数据库查询维护的时候不会因为敲错命令导致整个服务器雪崩。