Предисловие
В настоящее время я работаю над созданием конвейеров агрегации для сервера, который запрашивает последние доступные данные каждый раз, когда клиент запрашивает их. Это происходит очень часто (подумайте каждую секунду или около того), и поэтому мне интересно, сможете ли вы помочь мне оптимизировать конвейер, над которым я ломал голову. Я был бы очень признателен за любые предложения.
Задача
Предполагается, что конвейер запрашивает последнюю доступную запись для каждой b
в коллекции. Схема этой коллекции имеет следующую форму:
{
at: Date,
a: String,
b: String,
value: Any,
}
Мой текущий конвейер:
[
{
"$match": {
"a": {
"$in": [
"a001", "a002", "a003", "a004", "a005", "a006", "a007", "a008", "a009", "a010",
"a011", "a012", "a013", "a014", "a015", "a016", "a017"
]
},
"b": { "$exists": true },
"$and": [
{ "at": { "$gte": ISODate("2020-02-23T10:15:11.396Z") } },
{ "at": { "$lt": ISODate("2020-02-24T10:15:11.396Z") } }
]
}
},
{ "$sort": { "at": 1 } },
{
"$group": {
"_id": { "a": "$a", "b": "$b" },
"values": {
"$last": {
"k": "$a",
"v": { "at": "$at", "value": "$value" }
}
}
}
},
{
"$group": {
"_id": "$_id.b",
"entries": { "$push": "$values" }
}
},
{
"$project": {
"_id": false,
"b": "$_id",
"entries": { "$arrayToObject": "$entries" }
}
}
]
Условие at
используется для указания диапазона дат в пределах где найти последние записи. Порядок вставки ненадежен - поскольку он не обязательно соответствует упорядоченному списку at
значений - поэтому, насколько я могу судить, _id
для меня бесполезен.
Альтернативно условие "b": { "$exists": true }
также массив строк, соответствующих указанным c маякам, как a
.
Индекс определяется в полях a
, b
и at
:
{ at: 1, a: 1, b: 1 }
Теперь, к сожалению, имея 7 миллионов записей на данный момент - и считая быстро - чуть больше половины миллиона из которых соответствуют фильтру, этот запрос запускается работать очень медленно, обычно в диапазоне от 3 до 5 секунд , что, конечно, не особенно оптимально в производственной среде.
Запуск конвейера приводит к чему-то вроде следующего:
[
{
"b": "b001",
"entries": {
"a008": { "at": "2020-02-24T11:00:17.000Z", "value": {} },
"a012": { "at": "2020-02-24T11:00:08.000Z", "value": {} },
"a013": { "at": "2020-02-24T11:00:15.000Z", "value": {} }
}
},
{
"b": "b002",
"entries": {
"a010": { "at": "2020-02-24T11:00:10.000Z", "value": {} },
"a011": { "at": "2020-02-24T11:00:14.000Z", "value": {} },
"a008": { "at": "2020-02-24T11:00:17.000Z", "value": {} },
"a013": { "at": "2020-02-24T11:00:15.000Z", "value": {} },
"a009": { "at": "2020-02-24T11:00:10.000Z", "value": {} },
"a012": { "at": "2020-02-24T11:00:08.000Z", "value": {} }
}
},
{
"b": "b003",
"entries": {
"a008": { "at": "2020-02-24T09:48:24.000Z", "value": {} },
"a013": { "at": "2020-02-24a00:56:43.000Z", "value": {} },
"a012": { "at": "2020-02-24T11:00:08.000Z", "value": {} }
}
},
{
"b": "b004",
"entries": {
"a013": { "at": "2020-02-24T11:00:15.000Z", "value": {} },
"a012": { "at": "2020-02-24T11:00:08.000Z", "value": {} }
}
},
{
"b": "b005",
"entries": {
"a008": { "at": "2020-02-24T11:00:07.000Z", "value": {} },
"a013": { "at": "2020-02-24a00:33:28.000Z", "value": {} },
"a012": { "at": "2020-02-24T11:00:08.000Z", "value": {} }
}
},
{
"b": "b006",
"entries": {
"a010": { "at": "2020-02-24T11:00:10.000Z", "value": {} },
"a009": { "at": "2020-02-24T11:00:10.000Z", "value": {} },
"a001": { "at": "2020-02-24T09:27:14.000Z", "value": {} },
"a008": { "at": "2020-02-24T11:00:17.000Z", "value": {} },
"a013": { "at": "2020-02-24T11:00:15.000Z", "value": {} },
"a012": { "at": "2020-02-24T11:00:08.000Z", "value": {} },
"a011": { "at": "2020-02-24T11:00:14.000Z", "value": {} }
}
},
{
"b": "b007",
"entries": {
"a001": { "at": "2020-02-24a00:58:40.000Z", "value": {} }
}
},
{
"b": "b008",
"entries": {
"a010": { "at": "2020-02-24T11:00:10.000Z", "value": {} },
"a001": { "at": "2020-02-24T07:38:34.000Z", "value": {} },
"a012": { "at": "2020-02-24T11:00:08.000Z", "value": {} },
"a011": { "at": "2020-02-24T11:00:14.000Z", "value": {} },
"a013": { "at": "2020-02-24T11:00:15.000Z", "value": {} },
"a009": { "at": "2020-02-24T11:00:10.000Z", "value": {} },
"a008": { "at": "2020-02-24T11:00:17.000Z", "value": {} }
}
},
{
"b": "b009",
"entries": {
"a012": { "at": "2020-02-24a00:59:58.000Z", "value": {} }
}
},
{
"b": "b010",
"entries": {
"a012": { "at": "2020-02-24T11:00:08.000Z", "value": {} },
"a013": { "at": "2020-02-24a00:39:40.000Z", "value": {} }
}
},
{
"b": "b011",
"entries": {
"a005": { "at": "2020-02-24T11:00:08.000Z", "value": {} }
}
},
{
"b": "b012",
"entries": {
"a001": { "at": "2020-02-24T11:00:10.000Z", "value": {} },
"a011": { "at": "2020-02-24T11:00:14.000Z", "value": {} },
"a009": { "at": "2020-02-24T11:00:10.000Z", "value": {} },
"a008": { "at": "2020-02-24T11:00:17.000Z", "value": {} },
"a012": { "at": "2020-02-24T11:00:08.000Z", "value": {} },
"a013": { "at": "2020-02-24T11:00:15.000Z", "value": {} },
"a010": { "at": "2020-02-24a00:59:50.000Z", "value": {} }
}
},
{
"b": "b013",
"entries": {
"a008": { "at": "2020-02-24T11:00:17.000Z", "value": {} },
"a012": { "at": "2020-02-24T11:00:08.000Z", "value": {} },
"a013": { "at": "2020-02-24T11:00:15.000Z", "value": {} },
"a010": { "at": "2020-02-24T11:00:10.000Z", "value": {} },
"a011": { "at": "2020-02-24T11:00:14.000Z", "value": {} },
"a009": { "at": "2020-02-24T11:00:10.000Z", "value": {} }
}
},
{
"b": "b014",
"entries": {
"a012": { "at": "2020-02-24T11:00:08.000Z", "value": {} }
}
},
{
"b": "b015",
"entries": {
"a012": { "at": "2020-02-24T11:00:08.000Z", "value": {} },
"a013": { "at": "2020-02-24a00:12:24.000Z", "value": {} }
}
},
{
"b": "b016",
"entries": {
"a001": { "at": "2020-02-24T11:00:10.000Z", "value": {} },
"a005": { "at": "2020-02-24T11:00:08.000Z", "value": {} }
}
},
{
"b": "b017",
"entries": {
"a010": { "at": "2020-02-24T11:00:10.000Z", "value": {} },
"a008": { "at": "2020-02-24T11:00:17.000Z", "value": {} },
"a013": { "at": "2020-02-24T11:00:15.000Z", "value": {} },
"a012": { "at": "2020-02-24T11:00:08.000Z", "value": {} },
"a009": { "at": "2020-02-24T11:00:10.000Z", "value": {} },
"a001": { "at": "2020-02-24a00:45:28.000Z", "value": {} },
"a011": { "at": "2020-02-24T11:00:14.000Z", "value": {} }
}
},
{
"b": "b018",
"entries": {
"a011": { "at": "2020-02-24a00:34:29.000Z", "value": {} },
"a012": { "at": "2020-02-24T11:00:08.000Z", "value": {} },
"a008": { "at": "2020-02-24T11:00:17.000Z", "value": {} },
"a001": { "at": "2020-02-24T11:00:10.000Z", "value": {} },
"a013": { "at": "2020-02-24T11:00:15.000Z", "value": {} },
"a009": { "at": "2020-02-24T11:00:00.000Z", "value": {} },
"a010": { "at": "2020-02-24T11:00:10.000Z", "value": {} }
}
},
{
"b": "b019",
"entries": {
"a013": { "at": "2020-02-24a00:58:54.000Z", "value": {} },
"a012": { "at": "2020-02-24T11:00:08.000Z", "value": {} }
}
},
{
"b": "b020",
"entries": {
"a001": { "at": "2020-02-24T11:00:10.000Z", "value": {} },
"a005": { "at": "2020-02-24T11:00:08.000Z", "value": {} }
}
},
{
"b": "b021",
"entries": {
"a012": { "at": "2020-02-24a00:41:54.000Z", "value": {} },
"a001": { "at": "2020-02-24T11:00:10.000Z", "value": {} },
"a005": { "at": "2020-02-24a00:59:28.000Z", "value": {} },
"a013": { "at": "2020-02-24a00:47:21.000Z", "value": {} },
"a008": { "at": "2020-02-24T09:09:47.000Z", "value": {} }
}
},
{
"b": "b022",
"entries": {
"a001": { "at": "2020-02-24a00:59:50.000Z", "value": {} },
"a005": { "at": "2020-02-24T11:00:08.000Z", "value": {} }
}
}
]
Несоответствие дат в этих результатах и запросе / объяснении, потому что я сделал это в другое время. Это не ошибка.
Объяснение конвейера приводит к следующему:
{
"stages": [
{
"$cursor": {
"query": {
"a": {
"$in": [
"a001", "a002", "a003", "a004", "a005", "a006", "a007", "a008", "a009", "a010",
"a011", "a012", "a013", "a014", "a015", "a016", "a017"
]
},
"b": { "$exists": true },
"$and": [
{ "at": { "$gte": ISODate("2020-02-23T10:15:11.396Z") } },
{ "at": { "$lt": ISODate("2020-02-24T10:15:11.396Z") } }
]
},
"sort": { "at": 1 },
"fields": { "at": 1, "b": 1, "a": 1, "value": 1, "_id": 0 },
"queryPlanner": {
"plannerVersion": 1,
"namespace": "database.collection",
"indexFilterSet": false,
"parsedQuery": {
"$and": [
{ "at": { "$lt": ISODate("2020-02-24T10:15:11.396Z") } },
{ "at": { "$gte": ISODate("2020-02-23T10:15:11.396Z") } },
{ "b": { "$exists": true } },
{
"a": {
"$in": [
"a001", "a002", "a003", "a004", "a005", "a006", "a007", "a008", "a009", "a010",
"a011", "a012", "a013", "a014", "a015", "a016", "a017"
]
}
}
]
},
"queryHash": "EEAA11B5",
"planCacheKey": "C2A53FFA",
"winningPlan": {
"stage": "FETCH",
"filter": { "b": { "$exists": true } },
"inputStage": {
"stage": "IXSCAN",
"keyPattern": { "at": 1, "a": 1, "b": 1 },
"indexName": "at_1_a_1_b_1",
"isMultiKey": false,
"multiKeyPaths": { "at": [], "a": [], "b": [] },
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": 2,
"direction": "forward",
"indexBounds": {
"at": [ "[new Date(1582452911396), new Date(1582539311396))" ],
"a": [
"[\"a001\", \"a001\"]",
"[\"a002\", \"a002\"]",
"[\"a003\", \"a003\"]",
"[\"a004\", \"a004\"]",
"[\"a005\", \"a005\"]",
"[\"a006\", \"a006\"]",
"[\"a007\", \"a007\"]",
"[\"a008\", \"a008\"]",
"[\"a009\", \"a009\"]",
"[\"a010\", \"a010\"]",
"[\"a011\", \"a011\"]",
"[\"a012\", \"a012\"]",
"[\"a013\", \"a013\"]",
"[\"a014\", \"a014\"]",
"[\"a015\", \"a015\"]",
"[\"a016\", \"a016\"]",
"[\"a017\", \"a017\"]"
],
"b": [ "[MinKey, MaxKey]" ]
}
}
},
"rejectedPlans": []
},
"executionStats": {
"executionSuccess": true,
"nReturned": 511710,
"executionTimeMillis": 3410,
"totalKeysExamined": 559174,
"totalDocsExamined": 511710,
"executionStages": {
"stage": "FETCH",
"filter": { "b": { "$exists": true } },
"nReturned": 511710,
"executionTimeMillisEstimate": 583,
"works": 559174,
"advanced": 511710,
"needTime": 47463,
"needYield": 0,
"saveState": 4476,
"restoreState": 4476,
"isEOF": 1,
"docsExamined": 511710,
"alreadyHasObj": 0,
"inputStage": {
"stage": "IXSCAN",
"nReturned": 511710,
"executionTimeMillisEstimate": 334,
"works": 559174,
"advanced": 511710,
"needTime": 47463,
"needYield": 0,
"saveState": 4476,
"restoreState": 4476,
"isEOF": 1,
"keyPattern": { "at": 1, "a": 1, "b": 1 },
"indexName": "at_1_a_1_b_1",
"isMultiKey": false,
"multiKeyPaths": { "at": [], "a": [], "b": [] },
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": 2,
"direction": "forward",
"indexBounds": {
"at": [ "[new Date(1582452911396), new Date(1582539311396))" ],
"a": [
"[\"a001\", \"a001\"]",
"[\"a002\", \"a002\"]",
"[\"a003\", \"a003\"]",
"[\"a004\", \"a004\"]",
"[\"a005\", \"a005\"]",
"[\"a006\", \"a006\"]",
"[\"a007\", \"a007\"]",
"[\"a008\", \"a008\"]",
"[\"a009\", \"a009\"]",
"[\"a010\", \"a010\"]",
"[\"a011\", \"a011\"]",
"[\"a012\", \"a012\"]",
"[\"a013\", \"a013\"]",
"[\"a014\", \"a014\"]",
"[\"a015\", \"a015\"]",
"[\"a016\", \"a016\"]",
"[\"a017\", \"a017\"]"
],
"b": [ "[MinKey, MaxKey]" ]
},
"keysExamined": 559174,
"seeks": 47464,
"dupsTested": 0,
"dupsDropped": 0
}
}
}
}
},
{
"$group": {
"_id": { "a": "$a", "b": "$b" },
"values": {
"$last": {
"k": "$a",
"v": { "at": "$at", "value": "$value" }
}
}
}
},
{
"$group": {
"_id": "$_id.b",
"entries": { "$push": "$values" }
}
},
{
"$project": {
"_id": false,
"b": "$_id",
"entries": { "$arrayToObject": [ "$entries" ] }
}
}
],
"serverInfo": {
"host": "host",
"port": 27017,
"version": "4.2.3",
"gitVersion": "6874650b362138df74be53d366bbefc321ea32d4"
},
"ok": 1
}
Кажется важным следующее:
- The index
at_1_a_1_b_1
используется . Отлично. - Шаг сортировки оптимизирован с помощью MongoDB. Хорошо, умно, спасибо.
- время выполнения (
executionTimeMillis
) составляет 3.41s . Меньше звездного. - Всего проверенных ключей (
totalKeysExamined
) составляет 559174 . Полагаю, этого не происходит. Индексы должны быть проверены, чтобы определить, соответствуют ли они данному фильтру. - Всего проверенных и возвращенных документов (
totalDocsExamined
и nReturned
) оба 511710 . Об этом я абсолютно не знаю. Почему это так высоко? Почему на самом деле нужно проверять и возвращать все эти документы, когда я выбираю только $last
каждой группы? Странно, но, возможно, нормально.
Альтернатива
Если вы считаете, что конвейер настолько оптимален, насколько возможно, - я искренне надеюсь, что это не так, возможно, сервер просто недостаточно мощный, чтобы запускать его с такой высокой частотой запросов. Он имеет Intel Xeon X5670 и 32 ГБ оперативной памяти. Загрузка процессора MongoDB намного выше 100% при каждом запуске конвейера.