Створення конвеєрів агрегації MongoDB за допомогою $match, $group та $lookup
Дізнайтеся, як етапи агрегації MongoDB фільтрують, групують, перетворюють, об’єднують та сортують документи, а також як поєднувати їх у конвеєр, який дозволяє відповідати на реальні запитання зі звітності.
Звичайний запит find() підходить для отримання документів, які відповідають певній умові, але він не може підсумувати дохід за кожного клієнта, додавати деталі користувача до замовлень чи повертати звіт із ранжуванням. Для цього MongoDB пропонує механізм агрегації. У цьому посібнику пояснюються п’ять етапів, які ви будете використовувати найчастіше, невеликі підводні камені на кожному з них та способи їх поєднання в один потік обробки, який дає чистий та готовий до використання результат.
Як працює потік агрегації
Агрегація — це впорядкований список етапів. Документи потрапляють на перший етап, кожен етап трансформує потік певним чином, а результат його роботи стає вхідним даним для наступного етапу. Уявляючи це як конвеєр з обробки даних, легше зрозуміти порядок етапів.
Короткий потік обробки нижче зберігає лише завершені замовлення, підсумовує суму за кожного клієнта та виводить найбільших споживачів на перше місце.
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $group: { _id: "$customerId", total: { $sum: "$amount" } } },
{ $sort: { total: -1 } }
]);
У наступних розділах розглядається кожен етап окремо.
Фільтрація документів за допомогою $match
$match приймає той самий тип фільтра, який ви б передали до find(). У цьому прикладі лише ті документи, чий status дорівнює "completed", переходять до наступного етапу.
{
$match: {
status: "completed"
}
}
Розмістіть $match якомога раніше. Кожен документ, видалений на початку, — це один менший обсяг документів, які потрібно обробити на наступних етапах, а $match на початку конвеєра може використовувати індекси колекції, тоді як наступні етапи працюють з проміжними результатами, з якими індекси не можуть допомогти.
Групування та обчислення сум за допомогою $group
$group збирає документи, які мають спільний ключ, та обчислює значення для кожної групи. У прикладі створюється по одному документу на клієнта з сумою замовлення.
{
$group: {
_id: "$customerId",
totalSales: {
$sum: "$amount"
}
}
}
Роботу виконують дві частини:
_idвизначає ключ групування; тут це значення поляcustomerIdкожного документа (префікс$означає „читати це поле“).$sum— це акумулятор, який підсумовує значенняamountдля кожного документа в групі.
Інші акумулятори працюють за тим самим принципом, зокрема $avg, $min, $max та $count. Пам’ятайте, що результат $group містить лише _id та поля, які ви обчислили; усі інші поля оригінальних документів зникають.
Вибір та перетворення полів за допомогою $project
$project визначає, які поля будуть відображатися у результаті, та може створювати нові. Встановлення значення 1 для поля означає його включення, а _id: 0 прямо приховує ідентифікатор, який інакше включається за замовчуванням.
{
$project: {
customerId: 1,
totalSales: 1,
_id: 0
}
}
Він також може обчислювати значення. Тут $multiply створює поле totalWithTax, застосовуючи до totalSales коефіцієнт податку 18%.
{
$project: {
customerId: 1,
totalWithTax: {
$multiply: ["$totalSales", 1.18]
}
}
}
Є одна важлива деталь, коли ці фрагменти використовуються після етапу $group: ідентифікатор клієнта знаходиться у полі _id, а не у customerId, тому customerId: 1 не буде виводити жодних даних. Об’єднана конвеєрна схема вирішує цю проблему, записуючи customerId: "$_id", що змінює назву поля. Вважайте $project етапом, який формує кінцевий результат, що повертається вашим API.
Об’єднання іншої колекції за допомогою $lookup
Документи часто посилаються на дані, які зберігаються в іншому місці. У разі наявності колекції orders та колекції users, оператор $lookup отримує відповідного користувача для кожного замовлення.
{
$lookup: {
from: "users",
localField: "customerId",
foreignField: "_id",
as: "customer"
}
}
Для кожного замовлення MongoDB порівнює значення customerId замовлення з значенням _id у колекції users та записує всі знахідки у нове поле у вигляді масиву під назвою customer. Це є найближчим еквівалентом операції об’єднання в SQL. Результатом завжди є масив, навіть коли знаходиться лише один користувач, тому часто після операції пошуку використовують оператор $unwind або читають перший елемент масиву. Також необхідно переконатися, що обидва поля мають один і той самий тип: рядкове значення customerId не може збігатися з значенням типу ObjectId у полі _id.
Впорядкування результатів за допомогою $sort
$sort сортує документи за однією або кількома полями. Значення 1 використовується для сортування у зростаючому порядку, а -1 — у спадному, тож у прикладі клієнти розташовані від найвищого значення totalSales до найнижчого.
{
$sort: {
totalSales: -1
}
}
Об’єднання етапів у один звіт
Справжня користь проявляється, коли етапи поєднуються між собою. Наведена нижче конфігурація перетворює первинні замовлення на відсортований список клієнтів та їхнього загального обсягу продажів:
db.orders.aggregate([
{
$match: {
status: "completed"
}
},
{
$group: {
_id: "$customerId",
totalSales: {
$sum: "$amount"
}
}
},
{
$sort: {
totalSales: -1
}
},
{
$project: {
customerId: "$_id",
totalSales: 1,
_id: 0
}
}
]);
Крок за кроком вона:
- Зберігає лише завершені замовлення за допомогою
$match. - Групує їх за клієнтами за допомогою
$group. - Підсумовує суми замовлень кожного клієнта у поле
totalSales. - Сортує клієнтів за цим показником, починаючи з найвищих значень.
- Використовує
$project, щоб перейменувати_idнаcustomerIdта вилучити_idз результату.
Щоб включити імена чи електронні адреси, можна додати операцію $lookup щодо таблиці users. Розміщення її після $group та будь-якого $limit означає, що об’єднання даних відбувається один раз на клієнта, а не один раз на замовлення.
Основні моменти
Короткий спосіб запам’ятати основні етапи:
$match— фільтрація даних.$group— групування та обчислення.$project— вибір та перетворення даних.$lookup— об’єднання колекцій.$sort— сортування результатів.
Разом вони охоплюють більшу частину того, що потрібно для звітів, панелей керування, точок аналітики та бізнес-правил після того, як ви вийдете за межі базових операцій CRUD. Порядок має значення: фільтруйте рано, групуйте один раз, об’єднуйте якомога пізніше, дозволяючи це даним, та формуйте результат востаннє. Під час інтерв’ю та переглядів дизайну здатність пояснити, чому певний етап знаходиться саме там, є переконливішою, ніж просте перелічення його функцій.
Пов’язана література
- Як запобігти втраті оновлень у Node.js та MongoDB під час одночасних записів — Дізнайтеся, як атомарні умовні оновлення, оптимістичне блокування на основі версій, відповіді 409 та транзакції запобігають тому, щоб одночасні записи в MongoDB мовчки видаляли дані.