Гайды
Индексы и aggregation pipeline в MongoDB
createIndex, compound и partial индексы, explain executionStats, стадии $match $lookup $group и оптимизация пайплайна.
~13 мин чтения
Индексы и aggregation pipeline в MongoDB
Без индексов MongoDB на больших коллекциях часто делает COLLSCAN. Aggregation Pipeline — основной способ отчётов, join-подобных операций и подготовки данных для API. Опирается на «Введение в MongoDB: CRUD и схемы»; про шарды и scatter-gather — «Репликация и шардирование MongoDB».
1. Зачем индексы
Индекс — структура (часто B-tree), сопоставляющая значение ключа списку документов. Планировщик выбирает IXSCAN вместо COLLSCAN, если фильтр/сорт покрываются индексом.
Цена: каждый индекс ускоряет свой класс запросов, но замедляет записи и занимает место.
2. Создание и просмотр
db.orders.createIndex({ customerId: 1 })
db.users.createIndex({ email: 1 }, { unique: true })
db.orders.createIndex({ customerId: 1, createdAt: -1 })
db.orders.createIndex(
{ createdAt: -1 },
{ partialFilterExpression: { status: "new" } }
)
db.sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 })
db.orders.getIndexes()
db.orders.dropIndex("customerId_1_createdAt_-1")
3. Типы индексов (обзор)
| Тип | Когда |
|---|---|
| Single-field | Один ключ фильтра/сортировки |
| Compound | Несколько полей; порядок полей и направлений должен соответствовать запросу |
| Multikey | Автоматически для массивов |
| Text | $text |
| 2dsphere | GeoJSON |
| Hashed | Часть shard key или равномерное распределение |
Правило ESR для compound: Equality → Sort → Range.
4. Покрытие запроса (covered)
db.orders.find(
{ customerId: "c-1" },
{ _id: 0, customerId: 1, status: 1 }
).explain("executionStats")
Смотрите totalDocsExamined vs nReturned, IXSCAN, PROJECTION_COVERED.
5. explain
db.orders.find({ customerId: "x" }).sort({ createdAt: -1 }).explain("executionStats")
executionStats.executionTimeMillistotalKeysExamined/totalDocsExaminedwinningPlan.stage— избегать COLLSCAN на больших коллекциях без необходимости
Для агрегации:
db.orders.aggregate(
[{ $match: { status: "paid" } }, { $group: { _id: "$customerId", n: { $sum: 1 } } }],
{ explain: true }
)
6. Pipeline: ментальная модель
Стадии идут последовательно; порядок сильно влияет на производительность.
| Стадия | Назначение |
|---|---|
$match | Фильтр — как можно раньше, с индексом |
$project / $set / $unset | Форма документа |
$lookup | Join с другой коллекцией |
$group | Агрегация по _id |
$sort | Большие сортировки могут требовать allowDiskUse |
$limit / $skip | skip больших смещений дорог — keyset-пагинация |
$facet | Несколько подпайплайнов |
$unionWith | Объединение с другой коллекцией |
Пример
db.orders.aggregate([
{
$match: {
status: "paid",
createdAt: { $gte: ISODate("2025-01-01"), $lt: ISODate("2026-01-01") }
}
},
{ $group: { _id: "$customerId", orders: { $sum: 1 }, revenue: { $sum: "$total" } } },
{ $sort: { revenue: -1 } },
{ $limit: 50 }
])
Индекс вроде { status: 1, createdAt: 1 } помогает первому $match.
7. $lookup
db.orders.aggregate([
{ $match: { status: "new" } },
{
$lookup: {
from: "customers",
localField: "customerId",
foreignField: "_id",
as: "customer"
}
},
{ $unwind: { path: "$customer", preserveNullAndEmptyArrays: true } }
])
На больших объёмах $lookup дорогой; иногда денормализация или два запроса выгоднее.
8. Оптимизация pipeline
$match/$sort, покрываемые индексом, в начале (проверяйтеexplain).- Узкий
$projectрано — меньше данных дальше. - Избегайте тяжёлого
$regexбез якоря. $groupпосле сильного$match.- Тяжёлые отчёты:
allowDiskUse: trueи контроль памяти на сервере.
9. explain и индексы под pipeline
Перед выкладкой отчёта в прод прогоняйте:
db.orders.aggregate([/* stages */], { explain: "executionStats" })
Смотрите totalDocsExamined vs nReturned, стадии IXSCAN vs COLLSCAN, executionTimeMillis. Если ранний $match не использует индекс — переставьте стадии или добавьте partial индекс под типичный фильтр.
10. Шардированные коллекции
$match должен по возможности включать shard key (или префикс), иначе scatter-gather по всем шардам. Подробности — в гайде по репликации и шардированию.