Подзапрос агрегации mongodb: адаптер Mongodb php

У меня есть коллекция ниже:

**S_SERVER**  –  **S_PORT**  –  **D_PORT**  –  **D_SERVER**  –  **MBYTES**
L0T410R84LDYL – 2481 – 139 – MRMCRUNCHAPP – 10
MRMCASTLE – 1904 – 445 – MRMCRUNCHAPP – 25
MRMXPSCRUNCH01 – 54769 – 445 – MRMCRUNCHAPP - 2
MRMCASTLE – 2254 – 139 – MRMCRUNCHAPP - 4
MRMCASTLE – 2253 – 445 – MRMCRUNCHAPP -35
MRMCASTLE – 987 – 445 – MRMCRUNCHAPP – 100
MRMCASTLE – 2447 – 445 – MRMCRUNCHAPP – 12
L0T410R84LDYL – 2481 – 139 – MRMCRUNCHAPP - 90
MRMCRUNCHAPP – 61191 – 1640 – OEMGCPDB – 10

Во-первых, мне нужны 30 лучших S_SERVER по общему количеству МБайт, переданных с каждого S_SERVER. Это я могу получить с помощью следующего запроса:

$sourcePipeline = array(
        array(
            '$group' => array(
                '_id' => array('sourceServer' => '$S_SERVER'),
                'MBYTES' => array('$sum' => '$MBYTES')
            ),
        ),
        array(
            '$sort' => array("MBYTES" => -1),
        ),
        array(
            '$limit' => 30
        )
    );
$sourceServers = $collection->aggregate($sourcePipeline);

Мне также нужны 30 лучших D_PORT по общему количеству МБАЙТ, переданных с каждого D_PORT для отдельного S_SERVER. Я делаю это, запуская цикл for из приведенных выше результатов серверов и получая их индивидуально один за другим для каждого S_SERVER.

$targetPortPipeline = array(
                array(
                    '$project' => array('S_SERVER' => '$S_SERVER', 'D_PORT' => '$D_PORT', 'MBYTES' => '$MBYTES')
                ),
                array(
                    '$match' => array('S_SERVER' => S_SERVER(find from above query, passed one by one in for loop)),
                ),
                array(
                    '$group' => array(
                        '_id' => array('D_PORT' => '$D_PORT'),
                        'MBYTES' => array('$sum' => '$MBYTES')
                    ),
                ),
                array(
                    '$sort' => array("MBYTES" => -1),
                ),
                array(
                    '$limit' => $limit
                )
            );
$targetPorts = $collection->aggregate($targetPortPipeline);

Но этот процесс занимает слишком много времени. Мне нужен эффективный способ получить требуемые результаты в том же запросе. Я знаю, что для этого использую php-адаптер Mongodb. Вы также можете сообщить мне функцию агрегации в формате javascript. Я конвертирую его в php.


person Shiv Aggarwal    schedule 15.05.2014    source источник


Ответы (1)


Ваша проблема здесь, по сути, заключается в том, что вы выдаете еще 30 запросов для ваших первоначальных 30 результатов. Для этого нет простого решения, и на данный момент кажется, что один запрос подходит, но есть несколько вещей, которые вы можете рассмотреть.

В качестве дополнительного примечания, вы не одиноки в этом, поскольку это вопрос, который я видел раньше, и который мы можем назвать «проблемой лучших N результатов». По сути, вам действительно нужен какой-то способ объединить два набора результатов, чтобы каждая граница группировки (исходный сервер) сама по себе имела только максимальные N результатов, в то время как на этом верхнем уровне вы также снова ограничиваете эти результаты до верхних N значений результатов.

Ваша первая агрегация запросит у вас результаты для 30 лучших «исходных серверов», которые вы хотите, и это нормально. Но вместо того, чтобы зацикливать дополнительные запросы из этого, вы можете попробовать создать массив только со значениями «исходного сервера» из этого результата и передать его во второй запрос, используя вместо этого оператор $in:

db.collection.aggregate([
    // Match should be first
    { "$match": { "S_SERVER": { "$in": sourceServers } } },

    // Group both keys
    { "$group": {
        "_id": {
            "S_SERVER": "$S_SERVER",
            "D_SERVER": "$D_SERVER"
        },
        "value": { "$sum": "$MBYTES" }
    }},

    // Sort in order of key and largest "MBYTES" 
    { "$sort": { "S_SERVER": 1, "value": -1 } }
])

Обратите внимание, что вы не можете «ограничить» здесь, так как он содержит каждый «исходный сервер» из начального совпадения. Вы также не можете «ограничить» границу группировки, которая, по сути, отсутствует в структуре агрегации, чтобы в противном случае это был результат двух запросов.

Поскольку он содержит каждый результат «целевого сервера» и, возможно, за пределами «30 лучших», вы будете обрабатывать результат в коде и пропускать возвращенные результаты после того, как «30 лучших» будут получены на каждом уровне группировки (исходный сервер). В зависимости от того, сколько результатов у вас есть, это может быть или не быть самым практичным решением.

Двигаясь дальше, где это не так практично, вы в значительной степени застряли с получением этого вывода в другую коллекцию в качестве промежуточного шага. Если у вас есть MongoDB версии 2.6 или выше, это может быть так же просто, как добавить этап конвейера $out в конце оператора. Для более ранних версий вы можете сделать эквивалентный оператор, используя mapReduce:

db.collection.mapReduce(
    function() {
        emit(
            {
                "S_SERVER": this["S_SERVER"],
                "D_SERVER": this["D_SERVER"]
            },
            this.MBYTES
        );
    },
    function(key,values) {
        return Array.sum( values );
    },
    {
        "query": { "S_SERVER": { "$in": sourceServers } },
        "out": { "replace": "output" }
    }
)

По сути, это тот же процесс, что и в предыдущем операторе агрегирования, но при этом следует отметить, что mapReduce не сортирует выходные данные. Это то, что покрывается дополнительной операцией mapReduce над результирующей коллекцией:

db.output.mapReduce(
    function() {

        if ( cServer != this._id["S_SERVER"] ) {
            cServer = this._id["S_SERVER"];
            counter = 0;
        }

        if ( counter < 30 )
            emit( this._id, this.value );

        counter++;
    },
    function(){},  // reducer is not actually called
    { 
        "sort": { "_id.S_SERVER": 1, "value": -1 },
        "scope": { "cServer": "", "counter": 0 }
    }
)

Реализация здесь представляет собой «серверную» версию «пропуска курсора», о которой упоминалось ранее. Таким образом, вы по-прежнему обрабатываете каждый результат, но возвращаемые результаты по сети ограничены 30 лучшими результатами на каждом «исходном сервере».

Как уже говорилось, все еще довольно ужасно, поскольку это должно «программно» сканировать результаты, чтобы отбросить те, которые вам не нужны, и, опять же, в зависимости от вашего объема, вам может быть лучше просто выдать aa .find() для каждое значение «исходный сервер» в этих результатах при сортировке и ограничении результатов

sourceServers.forEach(function(source) {
    var cur = db.output.find({ "_id.S_SERVER": source })
        .sort({ "value": -1 }).limit(30);
    // do something with those results
);

И это по-прежнему 30 дополнительных запросов, но, по крайней мере, вы не «агрегируете» каждый раз, поскольку эта работа уже сделана.

В качестве последнего примечания, которое на самом деле слишком много деталей, чтобы вдаваться в них полностью, вы могли бы подойти к этому, используя модифицированную форму первоначального запроса агрегации, который был показан. Это больше похоже на сноску: если вы дочитали до этого места, а другие подходы не кажутся разумными, то это, вероятно, наихудший вариант из-за ограничений памяти, которые могут возникнуть.

Лучший способ представить это — «идеальный» случай для агрегации «лучших N результатов», который, конечно, на самом деле не существует, но в идеале конец конвейера будет выглядеть примерно так:

    { "$group": {
        "_id": "$S_SERVER",
        "results": { 
            "$push": {
                "D_SERVER": "$_id.D_SERVER",
                "MBYTES": "$value"
            }, 
            "$limit": 30
        }
    }}

Таким образом, «несуществующий» фактор здесь — это возможность «ограничить» количество результатов, которые были «вставлены» в результирующий массив для каждого значения «исходного сервера». Мир, безусловно, был бы лучше, если бы эта или подобная функциональность была реализована, поскольку она делает решение таких проблем довольно простым.

Поскольку его не существует, вам остается прибегнуть к другим методам, чтобы получить тот же результат и получить списки, подобные этот пример, за исключением того, что в данном случае это намного сложнее.

Учитывая этот код, вы будете делать что-то вроде:

  1. Сгруппируйте все результаты обратно в массив для каждого сервера
  2. Сгруппируйте все это обратно в один документ
  3. Раскрутить первые результаты сервера
  4. Получите первую запись и снова сгруппируйтесь.
  5. Размотайте результаты снова
  6. Спроектируйте и сопоставьте найденную запись
  7. Отменить совпадающий результат
  8. Промойте и повторите шаги 4–7 30 раз.
  9. Сохраните первый серверный документ из 30 результатов
  10. Промойте и повторите для 3–9 для каждого сервера, то есть 30 раз.

На самом деле вы никогда не закодируете это напрямую, и вам придется кодировать этапы конвейера. Очень вероятно, что предел в 16 МБ превысит лимит, вероятно, не в самом конвейерном документе, но, скорее всего, в фактическом наборе результатов, поскольку вы помещаете все в массивы.

Вы также можете заметить, как легко было бы полностью взорваться, если бы ваши фактические результаты не содержали по крайней мере 30 лучших значений для каждого сервера.

Весь сценарий сводится к компромиссу, какой метод лучше всего подходит для ваших данных и соображений производительности:

  1. Живите с 30 запросами агрегации из вашего первоначального результата.
  2. Сократите до 2 запросов и отбросьте нежелательные результаты в клиентском коде.
  3. Вывод во временную коллекцию и использование пропуска курсора сервера для отмены результатов.
  4. Выполните 30 запросов из предварительно агрегированной коллекции.
  5. На самом деле потрудитесь реализовать конвейер, который выдает результаты.

В любом случае, из-за сложности агрегирования в целом, я бы предпочел периодически создавать ваш набор результатов и хранить его в своей собственной коллекции, а не пытаться делать это в режиме реального времени.

Данные не будут «последними» результатами, а только настолько свежими, насколько часто вы их обновляете. Но на самом деле извлечение этих результатов для отображения становится простым запросом, возвращающим максимум 900 довольно компактных результатов без накладных расходов на агрегирование для каждого запроса.

person Neil Lunn    schedule 16.05.2014
comment
Большое спасибо за вашу идею. Это сработало для меня в основном. Я получил большую часть результата с помощью агрегации и немного манипулировал им в конце PHP. Обязательно поделюсь ответом со всеми вами. - person Shiv Aggarwal; 19.05.2014