Главная / Статьи / Создание конвейеров агрегации в MongoDB с использованием $match, $group и $lookup

Создание конвейеров агрегации в MongoDB с использованием $match, $group и $lookup

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

1020 слов

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

Как работает цепочка агрегации

Агрегация представляет собой упорядоченный список стадий. Документы попадают в первую стадию, каждая стадия преобразует поток данных определенным образом, а результат её работы становится входными данными для следующей стадии. Если рассматривать это как конвейер обработки данных, становится проще понимать порядок стадий.

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

db.orders.aggregate([
  { $match: { status: "completed" } },
  { $group: { _id: "$customerId", total: { $sum: "$amount" } } },
  { $sort: { total: -1 } }
]);

В следующих разделах рассматривается каждый этап отдельно.

Фильтрация документов с помощью $match

$match принимает тот же тип фильтров, что и функция find(). В этом примере только те документы, у которых значение поля status равно "completed", переходят к следующему этапу.

{
  $match: {
    status: "completed"
  }
}

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

Группировка и вычисление общих значений с помощью $group

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

{
  $group: {
    _id: "$customerId",
    totalSales: {
      $sum: "$amount"
    }
  }
}

Работу выполняют две части:

  • _id определяет ключ группировки; здесь это значение поля customerId каждого документа (префикс $ означает «читать это поле»).
  • $sum — накопитель, который суммирует значение поля amount для всех документов в группе.

Другие накопители работают по тому же принципу, включая $avg, $min, $max и $count. Имейте в виду, что результат работы $group содержит только _id и вычисленные вами поля; все остальные поля исходных документов исчезают.

Выбор и переформатирование полей с помощью $project

$project определяет, какие поля будут отображаться в результате, и может создавать новые. Установка значения поля в 1 приводит к его включению, а значение _id: 0 явно скрывает идентификатор, который в противном случае включается по умолчанию.

{
  $project: {
    customerId: 1,
    totalSales: 1,
    _id: 0
  }
}

Он также может вычислять значения. Здесь $multiply формирует поле totalWithTax, применяя коэффициент налога 18% к значению totalSales.

{
  $project: {
    customerId: 1,
    totalWithTax: {
      $multiply: ["$totalSales", 1.18]
    }
  }
}

Важен один момент, когда эти фрагменты используются после этапа $group: идентификатор клиента тогда находится в поле _id, а не в customerId; поэтому указание customerId: 1 не приведет к выводу ничего. Общий поток обработки в конце решает эту проблему, задавая customerId: "$_id", что позволяет переименовать поле. Считайте $project этапом, который формирует окончательный ответ, возвращаемый вашим API.

Слияние с другой коллекцией с помощью $lookup

Документы часто содержат ссылки на данные, хранящиеся в другом месте. При наличии коллекций orders и users оператор $lookup извлекает соответствующего пользователя для каждого заказа.

{
  $lookup: {
    from: "users",
    localField: "customerId",
    foreignField: "_id",
    as: "customer"
  }
}

Для каждого заказа MongoDB сравнивает значение customerId заказа с значением _id в коллекции users и сохраняет все найденные совпадения в новом массивном поле с именем customer. Это наиболее близкий аналог операции слияния в SQL. Результатом всегда является массив, даже если совпадает ровно один пользователь; поэтому часто после операции поиска используется оператор $unwind или считывается первый элемент. Также убедитесь, что оба поля имеют один и тот же тип: строковое значение customerId не сможет совпасть с значением типа ObjectId в поле _id.

Сортировка результатов с помощью $sort

$sort сортирует документы по одному или нескольким полям. Значение 1 приводит к возрастанию порядка, а -1 — к убыванию, поэтому в примере клиенты отсортированы от наибольшего значения totalSales до наименьшего.

{
  $sort: {
    totalSales: -1
  }
}

Объединение этапов в один отчет

Настоящая польза проявляется при объединении нескольких этапов. Приведенная ниже цепочка преобразует сырые заказы в отсортированный список клиентов и их общего объема продаж:

db.orders.aggregate([
  {
    $match: {
      status: "completed"
    }
  },
  {
    $group: {
      _id: "$customerId",
      totalSales: {
        $sum: "$amount"
      }
    }
  },
  {
    $sort: {
      totalSales: -1
    }
  },
  {
    $project: {
      customerId: "$_id",
      totalSales: 1,
      _id: 0
    }
  }
]);

Пошагово процесс выглядит следующим образом:

  1. С помощью $match сохраняются только завершенные заказы.
  2. С помощью $group они группируются по клиентам.
  3. Суммы сумм заказов каждого клиента суммируются в поле totalSales.
  4. Клиенты сортируются по этой сумме, от наибольшей к меньшей.
  5. С помощью $project поле _id переименовывается в customerId, а само поле _id удаляется из результата.

Чтобы включить имена или адреса электронной почты, можно использовать операцию $lookup с таблицей users. Размещение её после операции $group и любой операции $limit означает, что объединение данных происходит один раз на каждого клиента, а не один раз на каждый заказ.

Основные моменты

Краткий способ запомнить основные этапы:

  • $match — фильтрация.
  • $group — группировка и вычисления.
  • $project — выбор данных и их переформатирование.
  • $lookup — объединение коллекций.
  • $sort — сортировка результатов.

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

Связанные статьи

  • Создание системы аутентификации JWT, готовой к использованию в производстве, для API на Node.js — как хэшировать пароли, выдавать краткосрочные JWT, добавлять токены обновления и механизмы отзыва, разделять полномочия, защищаться от атак типа brute force и проверять корректную работу механизмов аутентификации при сбоях.