Как стать автором
Обновить

MongoDB: $or VS $in — что работает быстрее?

Время на прочтение 6 мин
Количество просмотров 8.4K
По катом будет совсем небольшое сравнение производительности MongoDB в случаях использования $or и $in логических операций в запросах. Надеюсь, что данная заметка сэкономит кому-нибудь рабочее время.

Тесты запускались на MongoDB 2.4.9
Допустим в MongoDB есть коллекция документов. Для упрощения понимания сути — пусть это будут документы именно с двумя полями.
$m = new MongoClient('mongodb://mongodb01,mongodb02,mongodba/?replicaSet=pkrs');
$mdb = $m->selectDB('test');
$collection = $mdb->selectCollection('test');
$collection->drop();
$collection->ensureIndex(array('i' => 1, 'j' => 1));
for ($i = 0; $i < 100; ++$i) {
    for ($j = 0; $j < 100; ++$j) {
        $collection->insert(array('i' => $i, 'j' => $j));
    }
}

В коллекции будет всего 10К документов. Да, тут можно было использовать batchInsert, но не хочу усложнять понимание основной сути заметки.

Необходимо регулярно (несколько раз в секунду) делать выборку до 1000 документов. Условием выборки является набор несвязанных пар i и j.
Т.к. я с MongoDB начал работать меньше месяца назад, то первое, что пришло в голову — это запрос вот такого вида:
$orArray = array();
for ($i = 0; $i < 10; ++$i) {
    for ($j = 0; $j < 100; ++$j) {
        $orArray[] = array('i' => $i, 'j' => $j);
    }
}

$query = array('$or' => $orArray);

То, что тут данные идут по порядку — это только для примера, чтобы не забивать голову бизнес логикой. В реальности, как я выше отметил, пары i и j друг с другом никак не связаны и идут в хаотичном порядке.
Попробовав выполнить этот запрос у меня широко раскрылись глаза от неприятного удивления — запрос выполнялся больше 2-х секунд! Выше в коде видно, что индекс при этом создан.
Это было вообще неприемлемо.
Я решил убедиться, что тут не сеть тормозит, а дело именно в запросе.
Для теста сделает вот такой запрос:
$query = array('i' => array('$lt' => 10), 'j' => array('$lt' => 100));

Результат по объему данных тот же самый, но запрос уже начинает выполняться 0.01 секунды.
Стало понятно, что нужно искать обходной путь. И он был найден. По логике запроса напрашивалось использование $in вместо $or. Но я не смог найти как использовать $in сразу по парам значений. Если такой способ есть, то буду очень благодарен за подсказку.
Раз я не знаю как сделать $in по двум полям, то введем искусственное поле следующим образом (слепим значения i и j через подчеркивание '_'):
$collection->ensureIndex(array('ij' => 1));
for ($i = 0; $i < 100; ++$i) {
    for ($j = 0; $j < 100; ++$j) {
        $collection->insert(array('i' => $i, 'j' => $j, 'ij' => $i.'_'.$j));
    }
}

И тогда запрос наш становится следующим:
$inArray = array();
for ($i = 0; $i < 10; ++$i) {
    for ($j = 0; $j < 100; ++$j) {
        $inArray[] = $i.'_'.$j;
    }
}
$query = array('ij' => array('$in' => $inArray));

И «о чудо!» мы получаем наши данные всего за 0.01 секунды (а ведь все начиналось с «больше 2-х секунд»).
Погуглив немного я нашел следующее объяснение данного феномена: при запросе с конструкцией $or MongoDB якобы делает несколько запросов и потом «мёржит» результаты. Не уверен в правоте данного утверждения, но другого пока не нашел.

P.S. Вывод: не злоупотребляйте $or
P.P.S. В коде ниже видно как я замерял время. Если кто-то не в курсе, то уточню, что при вызове find() запрос не выполняется! Создается только объект MongoCursor. И только уже при запросе первого документа идет сам запрос. Поэтому отсечки времени и снимаются на первой итерации цикла получения документов.
P.P.P.S. Если кому-то будет интересно погонять тесты у себя, то вот весь исходник:
<?php
$m = new MongoClient('mongodb://mongodb01,mongodb02,mongodba/?replicaSet=pkrs');
$mdb = $m->selectDB('test');
$collection = $mdb->selectCollection('test');
$collection->drop();
$collection->ensureIndex(array('i' => 1, 'j' => 1));
for ($i = 0; $i < 100; ++$i) {
    for ($j = 0; $j < 100; ++$j) {
        $collection->insert(array('i' => $i, 'j' => $j));
    }
}
$orArray = array();
for ($i = 0; $i < 10; ++$i) {
    for ($j = 0; $j < 100; ++$j) {
        $orArray[] = array('i' => $i, 'j' => $j);
    }
}

$query = array('$or' => $orArray);
testQuery('OR Query', $collection, $query);

$query = array('i' => array('$lt' => 10), 'j' => array('$lt' => 100));
testQuery('Range Query', $collection, $query);

$collection->drop();
$collection->ensureIndex(array('ij' => 1));
for ($i = 0; $i < 100; ++$i) {
    for ($j = 0; $j < 100; ++$j) {
        $collection->insert(array('i' => $i, 'j' => $j, 'ij' => $i.'_'.$j));
    }
}

$inArray = array();
for ($i = 0; $i < 10; ++$i) {
    for ($j = 0; $j < 100; ++$j) {
        $inArray[] = $i.'_'.$j;
    }
}
$query = array('ij' => array('$in' => $inArray));
testQuery('IN Query', $collection, $query);



function testQuery ($testName, $collection, $query) {
    $cursor = $collection->find($query);
    $cursor->batchSize(1000);
    $start = microtime(true);
    $first = true;
    foreach ($cursor as $doc) {
        if ($first) {
            $time1 = microtime(true);
            $first = false;
        }
    }
    $time2 = microtime(true);
    $resultFirst = $time1 - $start;
    $resultOther = $time2 - $time1;
    echo "{$testName} - First: {$resultFirst} Other: {$resultOther}<br />\n";

}


UPD 1 dim_s посоветовал вместо композитного (а из теста выше видно, что использовался именно композитный) индекса сделать два отдельных. Сделав так скорость обработки запроса ускорилась примерно в 10 раз (до 0.2 секунды), но все равно проигрывает варианту с $in

Выкладываю то, что выдает explain:
Explain ($or при композитном индексе)
/* 0 */
{
    "clauses" : [ 
        {
            "cursor" : "BtreeCursor i_1_j_1",
            "isMultiKey" : false,
            "n" : 1,
            "nscannedObjects" : 1,
            "nscanned" : 1,
            "nscannedObjectsAllPlans" : 1,
            "nscannedAllPlans" : 1,
            "scanAndOrder" : false,
            "indexOnly" : false,
            "nYields" : 0,
            "nChunkSkips" : 0,
            "millis" : 0,
            "indexBounds" : {
                "i" : [ 
                    [ 
                        1, 
                        1
                    ]
                ],
                "j" : [ 
                    [ 
                        1, 
                        1
                    ]
                ]
            }
        }, 
        {
            "cursor" : "BtreeCursor i_1_j_1",
            "isMultiKey" : false,
            "n" : 1,
            "nscannedObjects" : 1,
            "nscanned" : 1,
            "nscannedObjectsAllPlans" : 1,
            "nscannedAllPlans" : 1,
            "scanAndOrder" : false,
            "indexOnly" : false,
            "nYields" : 0,
            "nChunkSkips" : 0,
            "millis" : 0,
            "indexBounds" : {
                "i" : [ 
                    [ 
                        2, 
                        2
                    ]
                ],
                "j" : [ 
                    [ 
                        2, 
                        2
                    ]
                ]
            }
        }
    ],
    "n" : 2,
    "nscannedObjects" : 2,
    "nscanned" : 2,
    "nscannedObjectsAllPlans" : 2,
    "nscannedAllPlans" : 2,
    "millis" : 0,
    "server" : "mongodb01:27017"
}


Explain ($or при двух отдельных индексах по i и j)
/* 0 */
{
    "clauses" : [ 
        {
            "cursor" : "BtreeCursor i_1",
            "isMultiKey" : false,
            "n" : 1,
            "nscannedObjects" : 100,
            "nscanned" : 100,
            "nscannedObjectsAllPlans" : 300,
            "nscannedAllPlans" : 300,
            "scanAndOrder" : false,
            "indexOnly" : false,
            "nYields" : 0,
            "nChunkSkips" : 0,
            "millis" : 1,
            "indexBounds" : {
                "i" : [ 
                    [ 
                        1, 
                        1
                    ]
                ]
            }
        }, 
        {
            "cursor" : "BtreeCursor i_1",
            "isMultiKey" : false,
            "n" : 1,
            "nscannedObjects" : 100,
            "nscanned" : 100,
            "nscannedObjectsAllPlans" : 300,
            "nscannedAllPlans" : 300,
            "scanAndOrder" : false,
            "indexOnly" : false,
            "nYields" : 0,
            "nChunkSkips" : 0,
            "millis" : 1,
            "indexBounds" : {
                "i" : [ 
                    [ 
                        2, 
                        2
                    ]
                ]
            }
        }
    ],
    "n" : 2,
    "nscannedObjects" : 200,
    "nscanned" : 200,
    "nscannedObjectsAllPlans" : 600,
    "nscannedAllPlans" : 600,
    "millis" : 2,
    "server" : "mongodb01:27017"
}


Explain ($in при искусственно введенном индексе)
/* 0 */
{
    "cursor" : "BtreeCursor ij_1 multi",
    "isMultiKey" : false,
    "n" : 2,
    "nscannedObjects" : 2,
    "nscanned" : 3,
    "nscannedObjectsAllPlans" : 2,
    "nscannedAllPlans" : 3,
    "scanAndOrder" : false,
    "indexOnly" : false,
    "nYields" : 0,
    "nChunkSkips" : 0,
    "millis" : 0,
    "indexBounds" : {
        "ij" : [ 
            [ 
                "1_1", 
                "1_1"
            ], 
            [ 
                "2_2", 
                "2_2"
            ]
        ]
    },
    "server" : "mongodb01:27017"
}


UPD 2 Прогнал тест на свежей версии MongoDB 2.6
Действительно, исходный вариант (композитный индекс по двум полям) там работает значительно быстрее! А именно за 0.07 секунды. Но в тоже время вариант с индексом вида i_j все равно за 0.006 — 0.01 секунд (т.е. примерно раз в 10 быстрее)
Теги:
Хабы:
+16
Комментарии 12
Комментарии Комментарии 12

Публикации

Истории

Работа

PHP программист
175 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн