В прошлом уроке мы соединяли коллекции и разворачивали массивы. Каждый конвейер давал один отчёт. Сегодня три этапа для задач, которые обычным конвейером решаются неуклюже: несколько отчётов за один проход, раскладка значений по диапазонам и обход дерева произвольной глубины. Примеры на учебной базе магазина.
Витрина магазина
Открой каталог любого интернет-магазина. Слева фильтры: жанры с числом книг в каждом, ценовые диапазоны «до 500», «от 500 до 1000». Наверху «найдено 9 книг». В центре сами книги. Вверху страницы путь «Книги → Художественная → Фантастика».
Всё это разные запросы к одним и тем же данным. Можно отправить серверу четыре запроса, можно один. Сегодняшние этапы как раз для такой витрины.
$facet: несколько отчётов за один проход
$facet берёт поток документов и пускает его сразу в несколько подконвейеров. Каждый подконвейер работает со своей копией потока и не видит соседей. На выходе один документ, где под каждым именем лежит массив с результатом своего подконвейера.
Данные для витрины каталога:
db.books.aggregate([
{ $facet: {
total: [{ $count: 'books' }],
byGenre: [{ $group: { _id: '$genre', count: { $sum: 1 } } }, { $sort: { count: -1, _id: 1 } }],
cheapest: [
{ $match: { price: { $exists: true } } },
{ $sort: { price: 1 } },
{ $limit: 2 },
{ $replaceWith: { title: '$title', price: '$price' } }
]
} }
])
[
{
total: [ { books: 9 } ],
byGenre: [
{ _id: 'повесть', count: 3 },
{ _id: 'роман', count: 2 },
{ _id: 'учебник', count: 2 },
{ _id: 'фантастика', count: 2 }
],
cheapest: [
{ title: 'Шинель', price: 250 },
{ title: 'Собачье сердце', price: 390 }
]
}
]
Три отчёта, один запрос, один проход по коллекции. Обрати внимание: даже total, где одно число, лежит в массиве. Каждое поле $facet это результат конвейера, а конвейер всегда возвращает список.
Три правила $facet:
- Общий фильтр ставь до него. Если на витрине включён фильтр «только российские авторы»,
$matchидёт перед$facet, и все подконвейеры получат уже отобранные книги. - Внутри индексы не работают. Подконвейеры получают документы от предыдущего этапа, а не читают коллекцию сами. Тяжёлые выборки выгоднее делать отдельными запросами с индексами.
- Результат это один документ. А документ в MongoDB не может быть больше 16 МБ, как мы помним из урока про встраивание. Не выводи через
$facetвсе книги каталога, только верх списка.
$bucket: раскладка по корзинам
Ценовой фильтр на витрине: сколько книг до 500 рублей, сколько от 500 до 1000, сколько дороже. Одним $group это не решить: группировка идёт по точному значению, а нам нужны диапазоны.
$bucket раскладывает документы по диапазонам, которые ты задаёшь массивом границ:
db.books.aggregate([
{ $bucket: {
groupBy: '$price',
boundaries: [0, 500, 1000, 5000],
default: 'без цены',
output: { books: { $sum: 1 }, titles: { $push: '$title' } }
} }
])
[
{
_id: 0,
books: 3,
titles: [ 'Собачье сердце', 'Пикник на обочине', 'Шинель' ]
},
{
_id: 500,
books: 3,
titles: [ 'Мастер и Маргарита', 'Трудно быть богом', 'Мёртвые души' ]
},
{
_id: 1000,
books: 2,
titles: [ 'Грокаем алгоритмы', 'Совершенный код' ]
},
{ _id: 'без цены', books: 1, titles: [ 'Нос' ] }
]
Разберём параметры:
boundaries: [0, 500, 1000, 5000]даёт три корзины: от 0 до 500, от 500 до 1000, от 1000 до 5000. Нижняя граница входит в корзину, верхняя нет. Книга ровно за 500 рублей попала бы во вторую._idу корзины это её нижняя граница.defaultсобирает всё, что не попало ни в одну корзину. Здесь это «Нос» без цены. Безdefaultтакой документ вызвал бы ошибку.outputработает как аккумуляторы в$group. Если его не указать, будет толькоcount.
$bucket годится не только для цен. Книги по векам:
db.books.aggregate([
{ $bucket: { groupBy: '$year', boundaries: [1800, 1900, 2000, 2100], default: 'неизвестно', output: { books: { $sum: 1 } } } }
])
[
{ _id: 1800, books: 3 },
{ _id: 1900, books: 5 },
{ _id: 2000, books: 1 }
]
Когда границы придумывать не хочется, есть $bucketAuto: ты говоришь, сколько корзин нужно, а сервер подбирает границы так, чтобы документы легли поровну:
db.books.aggregate([
{ $bucketAuto: { groupBy: '$pages', buckets: 3 } }
])
[
{ _id: { min: 48, max: 224 }, count: 3 },
{ _id: { min: 224, max: 352 }, count: 3 },
{ _id: { min: 352, max: 896 }, count: 3 }
]
Корзины вышли некруглыми, зато по три книги в каждой. Для фильтра на витрине чаще берут $bucket с понятными людям границами, а $bucketAuto для аналитики.
$bucket и $facet часто идут вместе: ценовые корзины становятся одним из подконвейеров витрины.
урок из курса
Урок из курса «MongoDB с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.