Большой курс по MongoDB

Агрегация

Merrick
4 min readJan 11, 2018

Агрегация — это группировка значений многих документов. Операции агрегирования позволяют манипулировать такими сгруппированными данными например проводить подсчет по полям. В MySQL аналогом агрегации является команда group by. MongoDB предоставляет три способа выполнения агрегации: pipeline, Map-Reduce и одноцелевые методы агрегирования.

Pipeline

Фреймворк для агрегации в MongoDB моделирует концепцию обработку данных с помощью pipeline. Документы вводят многоэтапный конвейер, который преобразует документы в агрегированный результат.

Самые основные pipeline представляют фильтры, которые работают как запросы и преобразования документов. Другие pipeline это функции для группировки и сортировки документов по конкретному полю или нескольким полям, а также инструменты для агрегирования массивов. Также эти инструменты может обеспечивать выполнение таких задач как вычисление среднего числа или конкатенация строк.

Pipeline обеспечивает эффективную агрегацию данных с использование собственных операторов в MongoDB и является предпочтительным методом работы с агрегацией в MongoDB.

Pipeline агрегация можно работать и с sharded коллекциями. Что это мы узнаем позже.

Pipeline агрегация может использовать индексы для увеличения производительности на некоторых этапах. Кроме того pipeline агрегация имеет внутреннюю оптимизацию. Чтобы использовать агрегацию MongoDB предоставляет метод:

db.collection.aggregate()

Некоторые этапы pipeline принимают выражение конвейера в качестве операнда. Выражения конвейера указывают преобразование, применяемое к входным документам. Выражения имеют структуру документа и могут содержать другое выражение.

Выражения pipeline могут работать только с текущим документом в pipeline и не могут ссылаться на данные из других документов: операции выражения обеспечивают преобразование документов в оперативной памяти. Как правило, выражения не имеют состояния и оцениваются только при рассмотрении процесса агрегации с одним исключением: выражения аккумулятора.

Аккумуляторы, используемые на этапе $ group, сохраняют свое состояние (например, итоговые значения, максимумы, минимумы и связанные с ними данные) по мере продвижения документов по конвейеру.

Операторы $match и $sort могут использовать индекс, когда они встречаются в начале конвейера.

Представим, что у нас есть простая коллекция.

{ "_id" : ObjectId("5a5779894a531b514a44e7c9"), "name" : "Toster", "spec" : "prog", "lvl" : 5 }
{ "_id" : ObjectId("5a5779894a531b514a44e7ca"), "name" : "Johny", "spec" : "prog", "lvl" : 2 }
{ "_id" : ObjectId("5a5779894a531b514a44e7cb"), "name" : "Kostya", "spec" : "cook" }

Напишем простой агрегатор:

db.authors.aggregate({ $match: { spec: "prog" } })

Такой агрегатор вернет нам только два документа из трех. То есть оператор $match это фильтрация по определенному признаку, чтобы получить агрегацию по полю lvl нам нужно воспользоваться другим оператором. Также в $match мы можем использовать такие же операторы как и в методе find:

db.authors.aggregate(
{ $match: { spec: "prog" }},
{ $group: { _id: 'level', level: { $sum: '$lvl' } } }
)

Таким образом на выход мы получим документ:

{ "_id" : "level", "level" : 7 }

Оператор $group это и есть агрегатор, который вернет новый документ. У него есть собственные операторы, а поле текущего документа мы можем получить через знак $+название поля. Таким образом мы отфильтровали всех по полю spec и сделали сумму по полю $lvl.

Список всех операторов которые могут быть использованы в $group

  • $sum — Возвращает сумму всех численных полей.
  • $avg — Рассчитывает среднее значение между числовыми полями.
  • $min — получит минимальное значение из числовых полей
  • $max — Получить максимальное значение из числовых полей
  • $push — Помещает значение поля в результирующий массив
  • $addToSet — Вставляет значение в массив в результирующем документе, но не создаёт дубликаты.
  • $first — Получает только первый документ из сгрупированных, обычно используется с сортировкой.
  • $last — Получает последний документ

У оператора $group есть лимит по памяти в 100mb и есть процесс будет занимать больше, то оператор выдаст ошибку. Это можно изменить если использовать allowDiskUse. Есть способ облегчить выборку, это уменьшить количество ненужных полей с помощью оператора $project.

db.authors.aggregate(
{ $match: { spec: "prog" }},
{ $project: { lvl: 1 } },
{ $group: { _id: 'level', level: { $sum: '$lvl' } } }
)

Такая агрегация сначала отфильтрует все документы по полю spec, а затем удалит все поля из документа кроме lvl и _id, чтобы удалить id нужно явно указать _id: 0, и только после всего mongoDB сгруппирует документы.

Чтобы сортировать поля используется оператор $sort

db.authors.aggregate(
{ $match: { spec: "prog" }},
{ $project: { lvl: 1 } },
{ $sort: { lvl: 1 }}
)

Отсортирует документы по полю lvl.

Map-Reduce

map-reduce — это алгоритм предложенный гугл для обработки больших данных. Тут все довольно просто есть две функции одна map, которая удаляет поля в документах по определенным признакам и группирует их и функция reduce, которая может свернуть значение сгруппированных документов.

Представим, что у нас есть вот такая коллекция:

{ status: 'Frontend', salary: 1000, work: 'programmer'}
{ status: 'Backend', salary: 1300, work: 'programmer'}
{ status: 'Analitycs', salary: 1000, work: 'CTO'}

И задача, подсчитать среднюю зарплату по профессии независимо от его специальности, напишем следующий mapReduce:

const map = function () { emit( this.work, this.salary ) }
const reduce = function (key, values) {
return (Array.sum(values) / values.length);
}
db.worker.mapReduce(map, reduce, { out: 'mapReduceCollections' })

А теперь детально разберемся, что тут происходит!

Функция map, внутри получает функцию emit для группировки нашей коллекции. Представим, что сначала у нас есть пустой массив, зачем мы начинаем каждый документ в коллекции перебирать, MongoDB передает документ в функцию map, где мы вызываем функцию emit, которая добавляет в этот пустой массив новый объект, по ключу который мы передаем в первом аргументе, со значением которое мы передаем во втором аргументе, причем emit мы можем вызывать много раз. В итоге функция map создает нам следующий массив:

[
{ "_id" : "CTO", "value" : [1000] }
{ "_id" : "programmer", "value" : [1000, 1300] }
]

Если не понимаете, что происходит прочитайте еще раз.

После этого, функция reduce применится для каждого элемента массива сверху функцию reduce, которая в свою очередь вычислит среднее арифметическое всех элементов массива value и вернет.

[
{ "_id" : "CTO", "value" : 1000 }
{ "_id" : "programmer", "value" : 1150}
]

А затем этот массив поместиться в коллекцию, которую мы указали в out и мы можем позже проанализировать результат. Мощность данного алгоритма заключается в том, что в теории мы можем его распараллелить, что позволяет обрабатывать огромные массивы данных на множестве ядер/процессоров/машин.

Одноцелевая агрегация

Это агрегация одной коллекции по определенному ключу. Например:

db.users.count()

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

db.products.distinct('tag')

Таким образом мы получим список уникальных тегов в коллекции. Также мы можем передать вторым аргументом селектор, который преобразует подмножество коллекции products.

db.products.distinct('tag', { category: 'hats' })

--

--

Merrick

Переодически пишу про веб разработку. Любая поддержка сюда — https://www.tinkoff.ru/sl/3tsMbIw4Mqo