В прошлом уроке $group считал числа: сколько книг, какая средняя цена. Сегодня три шага дальше. Научим группу собирать списки, научим конвейер считать новые поля прямо в документе и впервые соединим две коллекции. Примеры, как обычно, на учебной базе магазина.
Аккумуляторы, которые собирают списки
Вернёмся к сортировочному центру. Раньше на посту группировки считали коробки в каждом районе. А можно составлять опись: какие посылки приехали, от каких отправителей, какая пришла первой. Для этого у $group есть ещё аккумуляторы.
Сгруппируем книги по автору и соберём о каждом сводку:
db.books.aggregate([
{ $sort: { year: 1, _id: 1 } },
{ $group: {
_id: '$author.name',
titles: { $push: '$title' },
genres: { $addToSet: '$genre' },
first: { $first: '$title' },
newest: { $max: '$year' }
} },
{ $sort: { _id: 1 } }
])
[
{
_id: 'Булгаков',
titles: [ 'Мастер и Маргарита', 'Собачье сердце' ],
genres: [ 'повесть', 'роман' ],
first: 'Мастер и Маргарита',
newest: 1987
},
...
{
_id: 'Гоголь',
titles: [ 'Нос', 'Шинель', 'Мёртвые души' ],
genres: [ 'роман', 'повесть' ],
first: 'Нос',
newest: 1842
},
...
]
Здесь четыре новых аккумулятора:
- $push складывает значение каждого документа группы в массив. Повторы остаются.
- $addToSet делает то же, но без повторов: у Гоголя три книги, а жанров два. Порядок элементов в таком массиве сервер не обещает, у тебя он может отличаться.
- $first берёт значение из первого документа группы. «Первый» имеет смысл только после сортировки, поэтому в начале конвейера стоит
$sortпо году. У Гоголя первым идёт «Нос» 1836 года. $maxс годом показывает самую свежую книгу автора. В пару к нему есть$min.
Заметь, что $push тоже сохранил порядок сортировки: книги Гоголя выстроились по году.
$project: придумать новые поля
С проекциями в find мы уже знакомы: 1 показать поле, 0 скрыть. Этап $project умеет то же, плюс главное: вычислять новые поля из выражений. Сколько стоит одна страница книги:
db.books.aggregate([
{ $match: { price: { $exists: true } } },
{ $project: { _id: 0, title: 1, price: 1, pricePerPage: { $round: [{ $divide: ['$price', '$pages'] }, 2] } } },
{ $sort: { pricePerPage: -1 } },
{ $limit: 3 }
])
[
{ title: 'Грокаем алгоритмы', price: 1290, pricePerPage: 4.48 },
{ title: 'Шинель', price: 250, pricePerPage: 3.91 },
{ title: 'Совершенный код', price: 2490, pricePerPage: 2.78 }
]
{ $divide: ['$price', '$pages'] } это выражение агрегации: оператор и массив аргументов. Здесь цена делится на число страниц, а $round округляет до двух знаков. $match в начале отбрасывает «Нос»: без цены делить нечего.
Короткая «Шинель» неожиданно оказалась одной из самых дорогих книг за страницу. Такой вопрос find не задашь: в документах нет поля «цена страницы», его посчитал конвейер.
$addFields: добавить, ничего не теряя
$project оставляет только перечисленные поля. Если нужно добавить поле и сохранить все остальные, есть $addFields. Сколько отзывов у книги:
db.books.aggregate([
{ $match: { _id: { $in: [1, 7] } } },
{ $addFields: { reviewsCount: { $size: { $ifNull: ['$reviews', []] } } } },
{ $project: { title: 1, reviewsCount: 1 } }
])
[
{ _id: 1, title: 'Мастер и Маргарита', reviewsCount: 2 },
{ _id: 7, title: 'Грокаем алгоритмы', reviewsCount: 2 }
]
$size считает элементы массива. У книг без отзывов поля reviews нет, и $size упал бы с ошибкой, поэтому $ifNull подставляет пустой массив.
урок из курса
Урок из курса «MongoDB с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.