Аккумуляторы, $project и $lookup

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

Аккумуляторы — функции, которые сводят группу документов к одному значению: сумма, среднее, минимум, максимум, количество.

Аккумуляторы в $group

Внутри $group кроме $sum доступны $avg (среднее), $min, $max и $push (собрать значения в массив). Статистика заказов по городам сразу по нескольким метрикам:

db.orders.aggregate([
  { $group: {
      _id: "$city",
      revenue:  { $sum: "$total" },
      avgCheck: { $avg: "$total" },
      maxOrder: { $max: "$total" },
      count:    { $sum: 1 }
  } }
])

За один проход получаем выручку, средний чек, самый крупный заказ и количество. В SQL это были бы SUM, AVG, MAX, COUNT.

$project — выбрать и вычислить поля

Этап $project формирует вид документов: какие поля оставить, как их переименовать, какие вычислить. Включаем 1, исключаем 0, а новые поля задаём выражением:

db.orders.aggregate([
  { $project: {
      _id: 0,
      city: 1,
      total: 1,
      withVat: { $multiply: ["$total", 1.2] }
  } }
])

Поле withVat вычисляется на лету — цена с НДС 20%. В $project доступна арифметика ($multiply, $add, $subtract), работа со строками и условия.

$lookup — соединение коллекций

Когда данные разнесены по коллекциям (ссылки!), их объединяют этапом $lookup — это и есть аналог LEFT JOIN. Подтянем к заказам данные пользователя:

db.orders.aggregate([
  { $lookup: {
      from: "users",
      localField: "userId",
      foreignField: "_id",
      as: "user"
  } }
])

Смысл полей: from — к какой коллекции присоединяем, localField — поле-ссылка в текущем документе, foreignField — поле в той коллекции, as — имя нового поля для результата. Подошедшие документы складываются в массив:

Результат:

{
  "_id": "o1",
  "userId": "u1",
  "total": 1800,
  "user": [ { "_id": "u1", "name": "Анна" } ]
}

Обратите внимание: user — это всегда массив (совпасть может несколько документов). Часто после $lookup добавляют $unwind, чтобы развернуть массив в отдельные документы.

$lookup стоит дорого

$lookup мощный, но небесплатный: соединение коллекций на больших объёмах медленнее, чем чтение одного встроенного документа. Поэтому, если данные почти всегда нужны вместе, в документной модели их предпочитают встраивать заранее, а $lookup приберечь для случаев, когда встраивание неуместно.

Итог

  • Аккумуляторы $sum, $avg, $min, $max сводят группу к числу внутри $group.
  • $project формирует вид результата и вычисляет новые поля выражениями.
  • $lookup — аналог JOIN между коллекциями; результат — массив, операция дорогая, поэтому встраивание часто предпочтительнее.
Проверьте себя
1. Какой аккумулятор вычисляет среднее значение в группе?
A$sum
B$avg
C$max
D$push
2. Что делает этап $lookup?
AСоздаёт индекс
BСоединяет документы текущей коллекции с документами другой по совпадению полей
CУдаляет дубликаты
DСортирует результат
3. В каком виде $lookup кладёт найденные документы в результат?
AВ виде одного объекта
BВ виде массива (поле as)
CВ виде строки
DПерезаписывает _id
Поддержать проект