Получите представление о своих данных в MongoDB путем агрегирования

MongoDB — это не просто база данных документов NoSQL. Вы можете выполнять сложный анализ документов в коллекциях, используя конвейеры агрегации. Распространенной задачей является группировка результатов и получение общих или средних данных для каждой группы. Вы даже можете фильтровать и конвертировать или очищать данные перед группировкой.

Конвейер агрегации состоит из одного или нескольких этапов, последовательно обрабатывающих документы. Каждый этап выполняет некоторую операцию над входными документами и передает обработанные на следующий этап. Например, на первом этапе можно фильтровать документы по некоторым условиям, на втором — группировать отфильтрованные документы и выполнять некоторые агрегации, а на третьем — выводить результаты.

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

В этом посте мы используем Docker для локального запуска сервера MongoDB, но вы можете использовать размещенный сервер, такой как MongoDB Atlas, который имеет бесплатный уровень и является хорошим выбором для учебных целей.

Когда приведенный выше код будет запущен, у нас будет коллекция laptops в базе данных products, содержащая 200 документов данных ноутбука. Документы имеют следующее содержание:

Теперь, когда данные готовы, мы можем начать писать конвейеры агрегации для анализа данных. Рекомендуется использовать MongoDB IDE для написания конвейеров, которые обычно охватывают несколько строк. С помощью IDE вы можете использовать автозаполнение кода и удобно писать запросы или конвейеры, охватывающие несколько строк. Однако в этом посте вы можете просто скопировать код, запустить его внутри mongosh и там проверить результат.

$group этап

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

$ docker exec -it mongo-server bash
$ mongosh "mongodb://admin:pass@localhost:27017"
test> use products
products> db.laptops.countDocuments()
200

Мы также можем использовать конвейер агрегации для подсчета количества документов, что сейчас кажется излишним, но позже он может послужить введением в более сложные конвейеры:

В нашем первом конвейере есть только один этап $group, который, как следует из названия, группирует документы по указанным _id. Обычно мы указываем поле для _id, чтобы группировать документы по этому полю. Однако, особенно когда _id равно null, это означает, что мы сгруппируем все документы вместе. Кроме того, total — это новое поле, содержащее данные, возвращаемые оператором-аккумулятором $count, который просто подсчитывает количество документов в каждой группе.

Давайте теперь сгруппируем по брендам и проверим число для каждого бренда:

Как упоминалось выше, на этапе $group _id задает поле для группировки входных документов. _id принимает в качестве значения выражение агрегации, которым могут быть пути к полям, системные переменные, объекты выражений и т. д.

"$brand" — это путь к полю и должен быть заключен в кавычки. Вы можете удивиться, почему мы должны ставить знак доллара $ перед именем поля. Да, это очень сбивает с толку, когда вы впервые видите это. Вы должны понимать, что _id принимает выражение, а не простое имя поля. И в выражении, если мы хотим указать поле, мы должны поставить перед ним знак доллара, иначе оно будет рассматриваться как литеральная строка.

Технически "$brand" — это сокращение для "$$CURRENT.brand", где CURRENT — это системная переменная, которая по умолчанию соответствует текущему документу. Как только вы это узнаете, "$brand" перестанет быть таким загадочным, не так ли?

$sort этап

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

После этапа $group добавляется новый этап $sort. Как мы уже знаем, отфильтрованные или агрегированные документы одного этапа передаются на следующий. В этом случае результаты группировки передаются на этап $sort. Поля исходных документов сейчас недоступны, доступны только те, что были на предыдущем этапе $group. Следовательно, мы можем упорядочить по полю total, которое было сгенерировано на предыдущем этапе $group. Кроме того, $sort принимает простую строку для имени поля, поэтому вам не нужно добавлять к ней префикс со знаком доллара.

$match этап

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

Мы можем использовать этап $match несколько раз в одном конвейере. Давайте выведем только те бренды, которых больше 10 в наличии:

$unwind этап

Теперь давайте представим более сложный этап, $unwind, который деконструирует поле массива из входных документов и выводит документ для каждого элемента исходного массива.

Для такого документа:

{"_id": 1, "name": "John", "scores": [70, 90, 80]}

На этапе $unwind на поле scores будут сгенерированы следующие документы:

{"_id": 1, "name": "John", "scores": 70}
{"_id": 1, "name": "John", "scores": 90}
{"_id": 1, "name": "John", "scores": 80}

Результирующие документы $unwind из того же массива будут иметь один и тот же первичный ключ _id. Давайте теперь размотаем поле attributes документов laptops, которое представляет собой массив атрибутивных документов:

Да, он раскручивается, как и ожидалось, и каждый новый документ содержит один атрибут исходного массива. Обратите внимание, что этап $unwind ожидает в качестве входных данных путь к полю, который должен быть указан с префиксом со знаком доллара, аналогичным полю _id для этапа $group.

Давайте теперь узнаем максимальный объем памяти для каждой марки ноутбука:

[
  { _id: 'HP', max_memory: '8GB' },
  { _id: 'Dell', max_memory: '8GB' },
  { _id: 'Asus', max_memory: '8GB' },
  { _id: 'Apple', max_memory: '8GB' },
  { _id: 'Lenovo', max_memory: '8GB' }
]

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

Интересно, что все бренды имеют максимальное значение «8 ГБ». Это связано с тем, что значение для памяти является строковым значением, а «8 ГБ» воспринимается как большее, чем «16 ГБ» или «32 ГБ». Нам нужно преобразовать его в числовое значение для сортировки, что может быть достигнуто с помощью этапа $project.

$project этап

Этап $project передает документы с запрошенными полями на следующий этап конвейера. Указанные поля могут быть существующими полями из входных документов или вновь вычисленными. В этом примере мы хотим передать марку как есть, но преобразовать значение строки памяти в числовое значение (в данном случае целое). Значение строки памяти содержит GB, и нам нужно удалить его, прежде чем его можно будет преобразовать в целое число, что можно сделать с помощью оператора $trim. Преобразование строки в целое число можно выполнить с помощью оператора $toInt.

Конечным используемым конвейером будет тот, который показан ниже. Обратите внимание, что этап $sort также используется дважды, как и этап $match, представленный выше.

И теперь память отсортирована правильно:

[
  { _id: 'HP', max_memory: 32 },
  { _id: 'Lenovo', max_memory: 32 },
  { _id: 'Dell', max_memory: 16 },
  { _id: 'Asus', max_memory: 16 },
  { _id: 'Apple', max_memory: 16 }
]

Мы можем продолжить анализ данных с различными комбинациями этапов. Тем не менее, это будет похоже на то, что мы представили. После того, как вы освоите основные и часто используемые этапы конвейеров агрегации, вы сможете выполнять все виды анализа, какие захотите. Рекомендуется написать некоторые пайплайны вручную и проверить вывод каждого этапа, чтобы лучше понять логику разных этапов.