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

    По катом будет совсем небольшое сравнение производительности 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 быстрее)
    Share post

    Comments 12

      0
      Хотя как написано в документации запросы с $or должны выполняться параллельно…
        +1
        А что говорит explain()? Попробуйте два отдельных индекса по i и j?
          0
          В пост добавил explain
            +1
            Здесь необходимо добавить уточнение: mongodb может использовать только один индекс для каждой операции, но в случае с $or, для каждой составляющей может быть использован отдельный индекс:
            MongoDB can only use one index to support any given operation. However, each clause of an $or query may use a different index.
            +7
            Мне всегда казалось, что для каждого элемента $or монго делает отдельный запрос, но если есть индексы, она должна их использовать, в вашем случае индекса должно быть 2 — на i и j, вы случайно не использовали копозитный индекс из i + j? Если это так, то индекс не будет задействован для $or.
              0
              Спасибо за совет. Сделал два индекса по i и j отдельно. Стало работать быстрее. Вместо 2 секунд стало 0.2 на данном тесте.
              Но $in все равно быстрее и выдает результат за 0.006 — 0.01 секунд (т.е. примерно раз в 50 быстрее).
              +1
              А не могли бы провести такие же тесты на 2.6? Я изучал недавно примерно такую же проблему, в 2.6 все было пошустрее с $or.
                0
                Проверил на 2.6
                Действительно, исходный вариант там работает значительно быстрее! А именно за 0.07 секунды. Но в тоже время вариант с индексом вида i_j все равно за 0.006 — 0.01 секунд (т.е. примерно раз в 10 быстрее)
                  0
                  Разница понятна, вы еще ищите по разным индексам. Предполагаю, что $in и $or по полю `i_j` должны давать один и тот же результат.
                    0
                    Ой какая странная монга :) $or по полю ij дает значительно хуже результат, чем $in по тому же самому полю: 0.35 — 0.40 (против 0.006 — 0.01 секунд)

                      0
                      Ничего странного, разные операции работают по разному за разное время.
                      Как выше написали $or — выполняет параллельно несколько запросов, а $in делает выборку за один проход (при правильном индексе).
                      $or может выбирать по разным условиям, $in работает по одному атрибуту.
                      Разные операции, разные задачи.
                0
                Раз я не знаю как сделать $in по двум полям, то введем искусственное поле следующим образом (слепим значения i и j через подчеркивание '_'):

                По идее можно попробовать поместить атрибуты в словарь { d:{ i:0, j:0 } }, и сделать поэтому словарю индекс.

                Only users with full accounts can post comments. Log in, please.