MongoDB: структура агрегации: получение последнего датированного документа по идентификатору группы

Я хочу получить последний документ для каждой станции со всеми другими полями:

{
        "_id" : ObjectId("535f5d074f075c37fff4cc74"),
        "station" : "OR",
        "t" : 86,
        "dt" : ISODate("2014-04-29T08:02:57.165Z")
}
{
        "_id" : ObjectId("535f5d114f075c37fff4cc75"),
        "station" : "OR",
        "t" : 82,
        "dt" : ISODate("2014-04-29T08:02:57.165Z")
}
{
        "_id" : ObjectId("535f5d364f075c37fff4cc76"),
        "station" : "WA",
        "t" : 79,
        "dt" : ISODate("2014-04-29T08:02:57.165Z")
}

Мне нужно иметь т и станции для последних DT на каждой станции. С структурой агрегации:

db.temperature.aggregate([{$sort:{"dt":1}},{$group:{"_id":"$station", result:{$last:"$dt"}, t:{$last:"$t"}}}])

возвращается

{
        "result" : [
                {
                        "_id" : "WA",
                        "result" : ISODate("2014-04-29T08:02:57.165Z"),
                        "t" : 79
                },
                {
                        "_id" : "OR",
                        "result" : ISODate("2014-04-29T08:02:57.165Z"),
                        "t" : 82
                }
        ],
        "ok" : 1
}

Это самый эффективный способ сделать это?

Спасибо

3 ответа

Решение

Чтобы прямо ответить на ваш вопрос, да, это самый эффективный способ. Но я думаю, что нам нужно уточнить, почему это так.

Как было предложено в альтернативах, одна вещь, на которую смотрят люди, это "сортировка" ваших результатов, прежде чем перейти к $group stage и то, на что они смотрят, это значение "timestamp", поэтому вы должны убедиться, что все находится в порядке "timestamp", поэтому отсюда и форма:

db.temperature.aggregate([
    { "$sort": { "station": 1, "dt": -1 } },
    { "$group": {
        "_id": "$station", 
        "result": { "$first":"$dt"}, "t": {"$first":"$t"} 
    }}
])

И как уже говорилось, вы, конечно, захотите, чтобы индекс отражал это, чтобы сделать сортировку эффективной:

Однако и это реальная точка. То, что, по-видимому, упустили из виду другие (если не для вас самих), - это то, что все эти данные, вероятно, вставляются уже во временном порядке, при этом каждое чтение записывается как добавленное.

Так что красота этого _id поле (по умолчанию ObjectId) уже находится в "метке времени", так как оно само содержит значение времени, и это делает возможным утверждение:

db.temperature.aggregate([
    { "$group": {
        "_id": "$station", 
        "result": { "$last":"$dt"}, "t": {"$last":"$t"} 
    }}
])

И это быстрее. Зачем? Ну, вам не нужно выбирать индекс (дополнительный код для вызова), вам также не нужно "загружать" индекс в дополнение к документу.

Мы уже знаем, что документы в порядке (по _id) Итак $last Границы совершенно действительны. В любом случае вы сканируете все, и вы также можете "запросить диапазон" на _id Значения одинаково действительны для двух дат.

Единственное реальное, что можно здесь сказать, это то, что при использовании в "реальном мире" для вас может быть более практичным $match между диапазонами дат при выполнении такого рода накопления, в отличие от получения "первого" и "последнего" _id значения, чтобы определить "диапазон" или что-то подобное в вашем фактическом использовании.

Так где же доказательство этого? Ну, это довольно легко воспроизвести, поэтому я просто сделал это путем генерации некоторых данных:

var stations = [ 
    "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL",
    "GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA",
    "ME", "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE",
    "NV", "NH", "NJ", "NM", "NY", "NC", "ND", "OH", "OK",
    "OR", "PA", "RI", "SC", "SD", "TN", "TX", "UT", "VT",
    "VA", "WA", "WV", "WI", "WY"
];


for ( i=0; i<200000; i++ ) {

    var station = stations[Math.floor(Math.random()*stations.length)];
    var t = Math.floor(Math.random() * ( 96 - 50 + 1 )) +50;
    dt = new Date();

    db.temperatures.insert({
        station: station,
        t: t,
        dt: dt
    });

}

На моем оборудовании (8-Гбайт ноутбук со спинным диском, который не является звездным, но, безусловно, адекватным), выполнение каждой формы оператора явно показывает заметную паузу с версией, использующей индекс и сортировку (те же ключи в индексе, что и у оператора сортировки). Это лишь небольшая пауза, но разница достаточно значительна, чтобы заметить.

Даже глядя на вывод объяснения (версии 2.6 и выше, или фактически есть в 2.4.9, хотя и не документированы), вы можете увидеть разницу в этом, хотя $sort оптимизируется из-за наличия индекса, кажется, что время, затрачиваемое на выбор индекса, а затем загрузку проиндексированных записей. Включение всех полей для "покрытого" индекса запроса не имеет значения.

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

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

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

db.temperature.aggregate([
    // Get documents "greater than" the "highest" _id value found last time
    { "$match": {
        "_id": { "$gt":  ObjectId("536076603e70a99790b7845d") }
    }},

    // Do the grouping with addition of the returned field
    { "$group": {
        "_id": "$station", 
        "result": { "$last":"$dt"},
        "t": {"$last":"$t"},
        "lastDoc": { "$last": "$_id" } 
    }}
])

И если вы на самом деле "следили" за такими результатами, то вы можете определить максимальное значение ObjectId от ваших результатов и использовать его в следующем запросе.

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

Индекс - это все, что вам действительно нужно:

db.temperature.ensureIndex({ 'station': 1, 'dt': 1 })
for s in db.temperature.distinct('station'):
    db.temperature.find({ station: s }).sort({ dt : -1 }).limit(1)

конечно, используя любой синтаксис, действительный для вашего языка.

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

db.temperature.aggregate([
    { $sort: { station: 1, dt: -1 } },
    { $group: { _id: "$station", result: {$first:"$dt"}, t: {$first:"$t"} } }
])

Что касается отправленного вами запроса на агрегацию, я должен убедиться, что у вас есть индекс для dt:

db.temperature.ensureIndex({'dt': 1 })

Это обеспечит максимальную эффективность сортировки $ в начале конвейера агрегации.

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

Однако, по мере того, как вы добавляете все больше и больше данных, проблема заключается в том, что запрос агрегации продолжит затрагивать все документы. Это будет становиться все дороже, поскольку вы масштабируете до миллионов или более документов. Один из подходов для этого случая - добавить $ limit сразу после $ sort, чтобы ограничить общее количество рассматриваемых документов. Это немного глупо и неточно, но это поможет ограничить общее количество документов, к которым необходимо получить доступ.

Другие вопросы по тегам