Конвейер агрегации «последний для всех отдельных идентификаторов» очень медленный, нужно создать правильные индексы? - PullRequest
0 голосов
/ 30 мая 2020

Учитывая следующий код конвейера агрегации для возврата самой новой записи для всех отдельных «internal_id»:

db.locations.aggregate({$sort: {timestamp: -1}}, {$group: {_id: "$internal_id", doc: {$first: "$$ROOT"}}})

Этот вызов занимает до 10 секунд, что неприемлемо. Коллекция не такая уж и большая:

db.locations.count()
1513671

Так что, я думаю, что-то не так с индексами, однако я попытался создать много индексов, и ни один из них не улучшился, в настоящее время я сохранил те два, которые должны были хватит imho: {timestamp: -1, internal_id: 1} и {internal_id: 1, timestamp: -1}.

MongoDB NOT сегментирован, и запущен набор реплик с 3 хостами версия 3.6.14.

Журнал MongoDB показывает следующее:

2020-05-30T12:21:18.598+0200 I COMMAND  [conn12652918] command mydb.locations appName: "MongoDB Shell" command: aggregate { aggregate: "locations", pipeline: [ { $sort: { timestamp: -1.0 } }, { $group: { _id: "$internal_id", doc: { $first: "$$ROOT" } } } ], cursor: {}, lsid: { id: UUID("70fea740-9665-4068-a2b5-b7b0f10dcde9") }, $clusterTime: { clusterTime: Timestamp(1590834060, 34), signature: { hash: BinData(0, 9DFB6DBCEE52CFA3A5832DC209519A8E9D6F1204), keyId: 6783976096153993217 } }, $db: "mydb" } planSummary: IXSCAN { timestamp: -1, ms_id: 1 } cursorid:8337712045451536023 keysExamined:1513708 docsExamined:1513708 numYields:11838 nreturned:101 reslen:36699 locks:{ Global: { acquireCount: { r: 24560 } }, Database: { acquireCount: { r: 12280 } }, Collection: { acquireCount: { r: 12280 } } } protocol:op_msg 7677msms

Ответы [ 2 ]

0 голосов
/ 02 июня 2020

Итак, наконец, я смог провести все тесты, вот вся версия, которую я написал, благодаря ответу Willis и результату:

Исходный совокупный запрос

mongo_query = [
  {"$match": group_filter},
  {"$sort": {"timestamp": -1}},
  {"$group": {"_id": "$internal_id", "doc": {"$first": "$$ROOT"}}},
]

res = mongo.db[self.factory.config.mongo_collection].aggregate(mongo_query)
res = await res.to_list(None)

9.61 секунд

Дайте MongoDB подсказку, чтобы использовать правильный индекс (сначала фильтр internal_id)

from bson.son import SON

cursor = mongo.db[self.factory.config.mongo_collection].aggregate(mongo_query, hint=SON([("internal_id", 1), ("timestamp", -1)]))
res = await cursor.to_list(None)

Не работает, MongoDB отвечает с исключением, говоря, что сортировка потребляет слишком много памяти

Разделение агрегации, чтобы сначала найти последнюю временную метку для каждого internal_id

cursor = mongo.db[self.factory.config.mongo_collection].aggregate([{"$group": {"_id": "$internal_id", "timestamp": {"$max": "$timestamp"}}}])
res = await cursor.to_list(None)

or_query = []
for entry in res:
    or_query.append({"internal_id": entry["_id"], "timestamp": entry["timestamp"]})
cursor = mongo.db[self.factory.config.mongo_collection].find({"$or": or_query})
fixed_res = await cursor.to_list(None)

1,88 секунды, намного лучше, но все же не так быстро

Параллельные сопрограммы (и победитель ...)

Между тем, поскольку у меня уже есть список internal_id и я использую асинхронный Python, я выбрал параллельную сопрограмму, получив последнюю запись для одного internal_id сразу:

fixed_res: List[Dict] = []

async def get_one_result(db_filter: Dict) -> None:
    """ Coroutine getting one result for each known internal ID """

    cursor = mongo.db[self.factory.config.mongo_collection].find(db_filter).sort("timestamp", -1).limit(1)
    res = await cursor.to_list(1)
    if res:
        fixed_res.append(res[0])

coros: List[Awaitable] = []
for internal_id in self.list_of_internal_ids:
    coro = get_one_result({"internal_id": internal_id})
    coros.append(coro)
await asyncio.gather(*coros)

0,5 с, намного лучше, чем другие

Если у вас нет списка internal_id

Есть альтернатива, которую я не реализовал, но я подтвердил, что вызов выполняется очень быстро: используйте низкий уровень distinct запятая nd по индексу {internal_id: 1} для получения списка индивидуальных идентификаторов, затем используйте параллельные вызовы.

0 голосов
/ 31 мая 2020

Mon go агрегации теоретически описательны (в том смысле, что вы описываете, что вы хотите, чтобы произошло, а оптимизатор запросов находит эффективный способ выполнения этого вычисления), но на практике многие агрегации оказываются процедурными и не оптимизированными. . Если вы посмотрите на инструкции по процедурной агрегации:

  1. {$sort: {timestamp: -1}}: отсортируйте все документы по метке времени.
  2. {$group: {_id: "$internal_id", doc: {$first: "$$ROOT"}}: go через эти отсортированные по меткам документы и затем сгруппируйте их по идентификатору. Поскольку на данный момент все отсортировано по метке времени (а не по идентификатору), это будет приличный объем работы.

Вы можете видеть, что это то, что на самом деле делает mon go взглянув на план запроса этой строки журнала: planSummary IXSCAN { timestamp: -1, ms_id: 1 }.

Вы хотите заставить mon go предложить лучший план запроса, чем тот, который использует индекс {internal_id: 1, timestamp: -1}. Предоставление ему подсказки для использования этого индекса может сработать - это зависит от того, насколько хорошо он способен вычислить план запроса.

Если предоставление этой подсказки не сработает, одна альтернатива будет чтобы разбить этот запрос на 2 части, каждая из которых использует соответствующий индекс.

  1. Найдите максимальную временную метку для каждой internal_id. db.my_collection.aggregate([{$group: {_id: "$internal_id", timestamp: {$max: "$timestamp"}}}]). Это должно использовать индекс {internal_id: 1, timestamp: -1}.
  2. Используйте эти результаты, чтобы найти документы, которые вам действительно интересны: db.my_collection.find({$or: [{internal_id, timestamp}, {other_internal_id, other_timestamp}, ....]}) (если есть повторяющиеся отметки времени для одного и того же internal_id, вам может потребоваться дедупликация).

Если вы хотите объединить эти 2 части в 1, вы можете использовать самосоединение в исходной коллекции с $lookup.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...