Раздел 36 · MongoDB с нуля

Этапы конвейера: $match, $group, $project, $sort

junior~25 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

В прошлом уроке мы подвели итоги первой половины раздела. Вторая начинается с конвейера агрегации, и начинается вглубь. В уроке про конвейер этапы стояли по одному-два, а выражения были простыми: $sum и $avg. Сегодня научимся считать внутри документа почти что угодно, перекраивать документ целиком и понимать, почему порядок этапов меняет и ответ, и цену запроса. Примеры на учебной базе магазина.

Кухня ресторана

Заказ в ресторане проходит несколько станций. Заготовщик моет и режет, повар жарит, кондитер украшает, официант выносит. Если поставить кондитера перед поваром, он украсит сырое мясо. Если заставить повара жарить весь холодильник, а потом выбросить лишнее, кухня встанет. Каждая станция делает своё дело, но результат зависит от того, в каком порядке они стоят.

Конвейер устроен так же. Этап получает документы предыдущего, меняет их и отдаёт дальше. Значит, у каждого этапа два вопроса: что он делает с документом и сколько документов до него доезжает.

Выражения: вычисления внутри документа

В уроке про аккумуляторы мы делили цену на число страниц через $divide. Это одно из сотен выражений агрегации. Разберём три семейства, которые встречаются чаще всего.

Условия: $cond и $switch

$cond работает как «если, то, иначе». Разделим книги на дорогие и обычные:

db.books.aggregate([
  { $set: { segment: { $cond: { if: { $gte: ['$price', 1000] }, then: 'дорогая', else: 'обычная' } } } },
  { $project: { _id: 0, title: 1, price: 1, segment: 1 } },
  { $sort: { price: -1, title: 1 } },
  { $limit: 4 }
])
[
  { title: 'Совершенный код', price: 2490, segment: 'дорогая' },
  { title: 'Грокаем алгоритмы', price: 1290, segment: 'дорогая' },
  { title: 'Мастер и Маргарита', price: 690, segment: 'обычная' },
  { title: 'Мёртвые души', price: 610, segment: 'обычная' }
]

$set здесь это то же самое, что $addFields из урока 18, просто короче. Он добавляет поле и не трогает остальные.

Когда веток больше двух, вложенные $cond читать тяжело. Для этого есть $switch: список условий и результат по умолчанию. У «Носа» цены нет, и для него нужна третья ветка:

db.books.aggregate([
  { $match: { _id: 9 } },
  { $set: { segment: { $switch: {
      branches: [
        { case: { $not: ['$price'] }, then: 'нет цены' },
        { case: { $gte: ['$price', 1000] }, then: 'дорогая' }
      ],
      default: 'обычная'
  } } } },
  { $project: { _id: 0, title: 1, segment: 1 } }
])
[ { title: 'Нос', segment: 'нет цены' } ]

$switch идёт по веткам сверху вниз и берёт первую подходящую, как CASE WHEN в SQL.

Массивы: $map и переменные

В заказе лежит массив позиций, у каждой количество и цена. Сумму заказа в документе никто не хранит, её надо посчитать: умножить количество на цену в каждой позиции и сложить.

$map проходит по массиву и для каждого элемента считает выражение. Элемент доступен через переменную, а переменные пишутся с двумя долларами:

db.orders.aggregate([
  { $set: { total: { $sum: { $map: {
      input: '$items',
      as: 'item',
      in: { $multiply: ['$$item.qty', '$$item.price'] }
  } } } } },
  { $project: { _id: 1, status: 1, total: 1 } },
  { $sort: { _id: 1 } }
])
[
  { _id: 1, status: 'delivered', total: 1980 },
  { _id: 2, status: 'delivered', total: 900 },
  { _id: 3, status: 'shipped', total: 2740 },
  { _id: 4, status: 'new', total: 1210 },
  { _id: 5, status: 'cancelled', total: 610 }
]

Читаем изнутри наружу. $map превращает массив позиций в массив сумм по позициям: у второго заказа две книги по 450 дают [900]. Внешний $sum складывает этот массив. Одинарный доллар '$items' это поле заказа, двойной '$$item.qty' это поле текущего элемента.

Строки: $concat и ловушка с кириллицей

Склеить строки помогает $concat. Число перед склейкой надо превратить в строку через $toString:

db.books.aggregate([
  { $project: { title: 1, label: { $concat: ['$title', ' (', { $toString: '$year' }, ')'] } } },
  { $limit: 2 }
])
[
  {
    _id: 1,
    title: 'Мастер и Маргарита',
    label: 'Мастер и Маргарита (1967)'
  },
  { _id: 2, title: 'Собачье сердце', label: 'Собачье сердце (1987)' }
]

А вот ловушка, о которую спотыкается каждый, кто пишет по-русски. $toUpper переводит в верхний регистр только латиницу:

db.books.aggregate([
  { $match: { price: { $gte: 1000 } } },
  { $set: { title: { $toUpper: '$title' } } },
  { $project: { _id: 0, title: 1 } }
])
[ { title: 'Грокаем алгоритмы' }, { title: 'Совершенный код' } ]

Сервер вернул названия без изменений. Документация прямо говорит: $toUpper и $toLower корректно работают только с ASCII. Для поиска без учёта регистра по-русски нужны регулярки с флагом i или правила сравнения (collation).

урок из курса

Урок из курса «MongoDB с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.