Что нужно знать об индексах в mongodb

Недавно закончил курс “M101P: MongoDB for Developers” (он периодически повторяется, например следующий стартует в апреле). В процессе прохождения натолкнулся на некоторые интересные моменты.

1. Выбор индекса для запроса.

Допустим у нас коллекция с такими данными:

{ "_id" : ..., "a" : 81810, "b" : 97482, "c" : 44288 }
{ "_id" : ..., "a" : 11734, "b" : 27893, "c" : 19485 }
// и т.д.

Всего 99999 объектов. У коллекции есть индексы:

db.foo.ensureIndex({a: 1, b: 1, c: 1})
db.foo.ensureIndex({c: -1})

Вопрос: какой индекс будет использован при запросе

db.foo.find({'a':{'$lt':10000}, 'b':{'$gt': 5000}}).sort({'c':-1}) 

?

Интуитивно очень хотелось бы, чтобы был использован индекс {a: 1, b: 1, c: 1}, ведь вроде бы он покрывает все нужные нам поля. Но, к сожалению, это не так.

Во-первых, индекс {a: 1, b: 1, c: 1} в этом случае не может быть использован одновременно для find и для sort, т.к. find содержит операторы сравнения ($lt, $gt). Т.е. в таком запросе

db.foo.find({'a': 1, 'b': 2}).sort({'c':-1})

индекс был бы использован полностью. Но, к сожалению, у нас не такой запрос.

Ну ладно, бог с ней с сортировкой, наверно индекс {a: 1, b: 1, c: 1} будет использован только для find, а сортировка уже будет сделана без индексов. Эх, смотрим:

db.foo.find({'a':{'$lt':10000}, 'b':{'$gt': 5000}}).sort({'c':-1}).explain()
{
    "cursor" : "BtreeCursor c_-1",
    "n" : 9498,
    "nscanned" : 99999,
    "scanAndOrder" : false,
    // другие поля не так интересны
}

{a: 1, b: 1, c: 1} даже не был задействован, вместо него индекс {c: -1} был использован для сортировки, потому что так решил mongodb’шный query optimizer.

Вот где пригодится принудительный выбор индексов оператором $hint:

db.foo.find({'a':{'$lt':10000}, 'b':{'$gt': 5000}}, {'a':1, 'c':1}).sort({'c':-1}).hint({a: 1, b: 1, c: 1}).explain()
{
    "cursor" : "BtreeCursor a_1_b_1_c_1",
    "n" : 9498,
    "nscanned" : 9974,
    "scanAndOrder" : true,
    // другие поля не так интересны
}

Сейчас индекс использовался для find, а сортировка осуществилась без индексов. Думаю, использовать индекс для фильтрации 9498 элементов из 99999 и затем к ним применить сортировку намного эффективнее нежели применить полный перебор к 99999 элементам и затем сортировку найденных 9498 осуществить с помощью индексов.

2. Направление индекса

Возвращаясь к предыдущему примеру, видно, что один из индексов имеет значение “-1”:

db.foo.ensureIndex({c: -1})

Это значит, что создан “нисходящий” индекс по полю “c”. Какое это вообще имеет значение?

db.foo.find().sort({'c':-1}).explain()
{
    "cursor" : "BtreeCursor c_-1",
    // ...
}

db.foo.find().sort({'c':1}).explain()
{
    "cursor" : "BtreeCursor c_-1 reverse",
    // ...
}

В обоих случаях применился индекс, только во втором случае он использовался в обратном порядке. Зачем тогда нужно указывать “направление” индекса?

Направление индекса пригодится при сортировке по двум и более полям:

db.foo.ensureIndex({a:1, b:1, c:1})

// тут индекс не может быть использован
db.foo.find().sort({a:-1, b:1}).explain()
{
    "cursor" : "BasicCursor",
    "scanAndOrder" : true,
    // ...
}

// а тут - может
db.foo.find().sort({a:1, b:1}).explain()
{
    "cursor" : "BtreeCursor a_1_b_1_c_1",
    "scanAndOrder" : false,
}

// но для сортировки по одному полю направление индекса не важно:
db.foo.find().sort({a:1}).explain()
{
    "cursor" : "BtreeCursor a_1_b_1_c_1",
    "scanAndOrder" : false,
}
db.foo.find().sort({a:-1}).explain()
{
    "cursor" : "BtreeCursor a_1_b_1_c_1 reverse",
    "scanAndOrder" : false,
}

Т.е. правило такое: при сортировке по двум и более полям направление сортировки должно совпадать с направлением индекса для всех полей.

3. Индексы и aggregation

Aggregation - очень, очень классная функция в mongodb. Было бы крайне полезно получать explain данные и об aggregation запросах. Такая возможность появится в версии 2.6 (на момент написания статьи эта версия официально еще не вышла).

Вот как можно будет применить explain в версии 2.6:

db.foo.aggregate([
        {$match: {a: {'$lt':10000}, b: {'$gt': 5000}}},
        {$sort: {c: -1}},
        {$group: {_id: null, a_total: {$sum: "$a"}}}
    ],
    {explain: true}
)

Но! Оказывается, explain можно использовать для aggregation и в текущей версии 2.4, только эта функция не документирована! Это можно сделать не напрямую, а используя runCommand:

db.foo.runCommand('aggregate', {pipeline:[
    {$match: {a: {'$lt':10000}, b: {'$gt': 5000}}},
    {$sort: {c: -1}},
    {$group: {_id: null, a_total: {$sum: "$a"}}}
], explain: true})

Вывод будет таким (сокращенно):

{
    "serverPipeline" : [
        {
            "query" : {
                "a" : {
                    "$lt" : 10000
                },
                "b" : {
                    "$gt" : 5000
                }
            },
            "sort" : {
                "c" : -1
            },
            // ...
            "cursor" : {
                "cursor" : "BtreeCursor c_-1",
                "n" : 9498,
                "nscanned" : 99999,
                "scanAndOrder" : false,
                // ..
            },
            // ...
    ],
    "ok" : 1
}

Вот правда hint к aggregation применить пока нельзя: SERVER-7944.

P.S.

Кстати, всем завершившим курс “M101P: MongoDB for Developers” выдают вот такой сертификат: M101P.