Перейти к основному содержимому

MongoDB - документоориентированная база данных

Разработчику Аналитику Тестировщику
Архитектору Инженеру

Перед чтением: Операторы — общие понятия оператора, операнда, приоритетов и типов операций; здесь те же идеи в синтаксисе запросов.


MongoDB

MongoDB — это распределённая, документо-ориентированная система управления базами данных с открытым исходным кодом, изначально разработанная для поддержки высоконагруженных, динамически изменяющихся и масштабируемых приложений. В отличие от реляционных СУБД, где данные организуются в строгие табличные структуры с фиксированными схемами, MongoDB основана на концепции документа — автономной, самодостаточной единицы данных, хранящейся в бинарном представлении JSON (BSON). Такой подход позволяет гибко работать со сложными, вложенными и неоднородными структурами без необходимости предварительного проектирования нормализованной схемы.

Play ITЗагрузка интерактивного демо…


Концептуальные основы

В реляционных СУБД ключевой строительный блок — строка таблицы — запись, состоящая из фиксированного набора полей, определённых заранее в схеме таблицы. Все строки одной таблицы имеют одинаковую структуру. В MongoDB аналогом строки является документ, а таблицы — коллекция. Однако принципиальное отличие в том, что коллекция не накладывает ограничений на структуру хранящихся в ней документов. Один документ может содержать поля name, age, hobbies; другой — лишь title и timestamp; третий — вложенный объект с произвольной глубиной вложенности. Такая гибкость относится к подходу schema-on-read: структура интерпретируется при чтении, а не задаётся жёстко заранее для всех записей. В классических реляционных СУБД доминирует schema-on-write — схема таблицы фиксируется до вставки. В MongoDB можно вводить валидаторы коллекции (частичная schema-on-write), но типичный сценарий — эволюция полей без миграции всей таблицы. Это особенно ценно при быстро меняющихся требованиях к данным.

Структура документа:

{
"_id": <ObjectId|значение>,
"<поле>": <значение>,
...
}

Каждый документ в MongoDB обязан содержать поле _id, играющее роль первичного ключа. Значением _id может быть любое допустимое BSON-значение, однако по умолчанию, если при вставке документа поле _id не указано, драйвер MongoDB генерирует его как ObjectId — 12-байтовое значение, состоящее из временной метки создания документа, идентификатора машины, идентификатора процесса и счётчика. Такая структура позволяет однозначно идентифицировать документы в распределённой среде без централизованного координатора и обеспечивает естественную сортировку по времени создания.

База данных в MongoDB — это логический контейнер, объединяющий одну или несколько коллекций, а также другие объекты — индексы, представления, пользовательские функции. База данных является основной единицей административного управления — на неё накладываются права доступа, политики резервного копирования, определяются параметры репликации. Одна инсталляция MongoDB (точнее, один сервер mongod или кластер mongos) может содержать множество баз данных, каждая со своей собственной логикой и набором коллекций.

Таким образом, иерархия организации данных в MongoDB выглядит следующим образом:

  • Сервер/Кластер MongoDB
    • База данных (Database)
      • Коллекция (Collection)
        • Документ (Document)
          • Поля (Fields) с BSON-значениями.

Эта иерархия напрямую отражается в синтаксисе оболочки MongoDB: db.collectionName.find(...) — где db — текущая база данных, а collectionName — её коллекция.

Реляционная СУБДMongoDB
Сервер / кластерmongod / replica set / sharded cluster
База данныхDatabase
ТаблицаCollection
СтрокаDocument
СтолбецПоле (field)

Репликация и шардирование

Replica set — несколько узлов mongod с одной логической БД: один primary принимает записи, secondary реплицируют oplog. При падении primary выбирается новый лидер (выборы). Для production обычно 3+ узла в разных зонах.

Почему "два живых узла" не выбирают primary

В наборе из пяти членов при доступности только двух ни один не станет primary: для выборов нужно большинство (≥3 из 5). Это защита от split-brain: при разрыве сети обе "половины" кластера не должны одновременно принимать записи — иначе данные разойдутся. MongoDB сознательно допускает период "только чтение", чтобы в приложении оставался один источник записи. Типовые топологии: большинство узлов в основном ЦОД или нечётное число площадок с "решающим" узлом в третьем месте. Подробнее — справочник §10, выборы и откаты oplog — там же.

Sharded cluster — данные режутся по shard key на несколько replica set (шарды). Клиент ходит в mongos (роутер), который направляет запрос на нужный шард. Шардирование нужно при росте объёма и записи beyond одного replica set. Общие принципы (типы шардинга, выбор ключа, маршрутизация) — в §9 "12 концепций"; детали MongoDB — в §11 шардирования.

Стратегия ключаПримерПлюсыРиски
Монотонный_id, timestamp, auto-incrementПростые range-запросы по времениВсе новые записи на одном шарде ("горячий" shard, firehose)
Hashed{ _id: "hashed" }Равномерное распределение записейRange-запросы по полю ключа затруднены
Compound + prefix{ tenantId: 1, createdAt: 1 }Изоляция арендатора, локальность данныхНужен prefix в запросах; кардинальность первого поля
Location / tag{ country: 1, userId: 1 } + zonesДанные рядом с приложением в регионеСложнее балансировка при смене географии

Для GridFS часто хешируют ключ шардинга по _id файла, чтобы крупные загрузки распределялись по шардам. См. §11 — firehose и hashed key.

Подробнее: справочник — репликация и шардирование.


Read Concern и Write Concern

При записи и чтении в кластере клиент задаёт, сколько узлов должны подтвердить операцию:

  • Write concern — например { w: "majority", j: true }: запись считается успешной после журналирования на большинстве реплик (меньше риск потери при сбое primary).
  • Read concern — например "majority" — чтение не вернёт данные, не зафиксированные на большинстве (строже, чем чтение с отстающей secondary).
  • Read preference — с какого узла читать — primary (по умолчанию), secondaryPreferred, nearest (по задержке).

В mongosh write concern часто передают в опциях insertOne / updateOne. В драйверах (Node.js, Python) те же параметры на уровне MongoClient или операции. Для учебного standalone на одном узле достаточно значений по умолчанию; в Atlas и production их настраивают осознанно.


BSON

MongoDB использует BSON (Binary JSON) — расширение JSON, оптимизированное для эффективного хранения и быстрого обхода. Хотя BSON часто называют "бинарным JSON", это самостоятельный двоичный формат с собственными типами данных и правилами сериализации.

Основные отличия BSON от обычного JSON:

  1. Типизация. В JSON существует ограниченный набор типов — строки, числа (не делятся на int/float), булевы значения, null, объекты и массивы. BSON, напротив, поддерживает более двадцати явных типов, включая:

    • 32-битные и 64-битные целые (int32, int64);
    • 32-битные и 64-битные числа с плавающей точкой (double, decimal128);
    • логические значения;
    • null;
    • строки;
    • двоичные данные (BinData);
    • объекты (Object);
    • массивы;
    • временные метки (Date);
    • регулярные выражения;
    • ObjectId;
    • минимум и максимум (MinKey, MaxKey);
    • JavaScript-код (Code) — устаревший, но поддерживаемый тип.

    Явная типизация позволяет избежать неоднозначностей при десериализации и обеспечивает корректную сортировку и сравнение (например, целое число 100 будет корректно сравниваться как число, а не как строка "100").

  2. Эффективность. BSON хранится в бинарной форме, что позволяет избежать накладных расходов на парсинг текстового JSON при каждом чтении или записи. Данные могут быть частично десериализованы без полной загрузки документа в память — например, драйвер может быстро извлечь значение _id, не обрабатывая остальное содержимое документа. Это критически важно для производительности в высоконагруженных системах.

  3. Поддержка двоичных данных. BSON включает специальный тип BinData, что делает возможным хранение изображений, аудио, видео или сериализованных объектов непосредственно в документах (хотя для очень больших бинарных объектов рекомендуется использовать GridFS — расширение MongoDB для хранения файлов).

  4. Самоописываемость. Каждое поле в BSON-документе предваряется своим типом и длиной, а весь документ начинается с 4-байтового заголовка, содержащего общую длину документа. Это позволяет быстро пропускать документы при сканировании и эффективно читать их в произвольном порядке.

Поскольку BSON является строгим надмножеством JSON, любой корректный JSON-объект может быть преобразован в BSON и обратно без потерь информации (при условии, что числа не теряют точности — например, целые вне диапазона int64 могут быть преобразованы в double). Обратное неверно: не каждый BSON-документ может быть корректно представлен в виде JSON без дополнительных аннотаций (например, Date или ObjectId в JSON требуют специального оборачивания в объект { "$date": "..." }).


Экосистема MongoDB

MongoDB — это не только ядро СУБД (mongod). Это развитая экосистема, включающая инструменты для разработки, администрирования, мониторинга и развёртывания.


Ядро и сервисы

  • mongod — основной процесс сервера базы данных. Он отвечает за хранение данных, обработку запросов, управление индексами, транзакциями, репликацией и шардированием. Может работать как в standalone-режиме, так и в составе реплика-сета или шардированного кластера.
  • mongos — маршрутизатор запросов для шардированных кластеров. Он принимает запросы от клиентов, определяет, к каким шардам они относятся, распределяет их и агрегирует результаты.
  • mongosh — современная интерактивная оболочка MongoDB, заменяющая устаревший mongo. Поддерживает синтаксис JavaScript, автодополнение, подсветку синтаксиса, историю команд и расширения. Является основным инструментом для интерактивной диагностики и администрирования.
  • MongoDB Atlas — облачная fully-managed платформа MongoDB, предоставляемая компанией MongoDB Inc. Управляет инфраструктурой, репликацией, шардированием, резервным копированием, обновлениями, мониторингом и безопасностью. Поддерживает развёртывание в AWS, Azure, Google Cloud.

Административные и разработческие инструменты

  • MongoDB Compass — официальный графический интерфейс для MongoDB, позволяющий визуально исследовать данные, строить запросы без знания синтаксиса, анализировать производительность запросов, создавать и управлять индексами, настраивать схемы и валидаторы. Особенно полезен для анализа распределения значений полей и выявления "холодных" или "горячих" участков данных.
  • MongoDB Shell Extensions — плагины для mongosh, расширяющие его функциональность — поддержка TypeScript, улучшенное форматирование, интеграция с Atlas и т.д.
  • MongoDB Database Tools — набор утилит командной строки — mongodump/mongorestore (резервное копирование и восстановление), mongoexport/mongoimport (экспорт/импорт в JSON или CSV), bsondump (просмотр BSON в человекочитаемом виде), mongofiles (управление GridFS).

Драйверы и ODM/ORM

MongoDB предоставляет официальные драйверы для всех основных языков — C, C++, C#, Go, Java, Node.js, Perl, PHP, Python, Ruby, Rust, Scala. Драйверы реализуют протокол обмена с сервером, обработку соединений, сериализацию BSON и базовую логику управления транзакциями.

Помимо драйверов, существуют высокоуровневые абстракции:

  • Mongoose (Node.js) — popular ODM (Object Data Mapper), добавляющий строгую схему, валидацию, middleware и методы-хелперы.
  • PyMongo + MongoEngine (Python) — PyMongo — низкоуровневый драйвер, MongoEngine — ODM поверх него.
  • Spring Data MongoDB (Java) — часть экосистемы Spring, предоставляет шаблоны репозиториев, аннотации для сопоставления объектов и интеграцию с транзакционным менеджером Spring.

Дополнительные компоненты

  • Atlas App Services (ранее MongoDB Realm / Stitch) — бэкенд в облаке Atlas — триггеры, HTTP-эндпоинты, синхронизация с мобильными клиентами через Atlas Device SDK.
  • MongoDB Charts — инструмент визуализации данных, позволяющий строить графики и дашборды по данным MongoDB без написания кода.
  • MongoDB Kafka Connector — интеграция с Apache Kafka для потоковой передачи изменений в MongoDB (Change Streams → Kafka).

Эта экосистема позволяет охватить весь жизненный цикл данных: от проектирования и разработки приложения до развёртывания, мониторинга и аналитики.


CRUD-операции

Сквозной практикум в mongosh (insert → find → index → aggregate): Первые шаги с MongoDB.

Операции создания, чтения, обновления и удаления в MongoDB реализованы через методы, работающие на уровне коллекций. Все они принимают BSON-объекты в качестве параметров и возвращают структурированные результаты (включая метаданные — количество затронутых документов, статус записи, время выполнения при профилировании и т.п.).


Вставка — insertOne и insertMany

Метод insertOne(document, options?) вставляет один документ в коллекцию. Если в документе отсутствует поле _id, драйвер автоматически генерирует для него ObjectId. Если _id указан явно и уже существует в коллекции, операция завершается ошибкой E11000 duplicate key error, поскольку _id всегда имеет уникальный индекс.

Метод insertMany(documentsArray, options?) позволяет вставить массив документов за одно обращение к серверу. Это существенно эффективнее, чем N вызовов insertOne, особенно при работе по сети. По умолчанию вставка выполняется последовательно и атомарно для каждого документа, но не для всего массива в целом — если при вставке k-го документа возникнет ошибка (например, дубликат _id), операция прекращается, и последующие документы не вставляются. Это поведение контролируется флагом ordered:

  • ordered: true (по умолчанию) — остановка при первой ошибке;
  • ordered — false — сервер продолжает обработку оставшихся документов, несмотря на ошибки, и возвращает агрегированную информацию обо всех сбоях.
<коллекция>.insertOne(<документ>[, <опции>])
<коллекция>.insertMany([<документ>, ...][, <опции>])

Оба метода принимают опцию writeConcern, определяющую уровень подтверждения записи. Например, { w: "majority", wtimeout: 5000 } означает: дождаться подтверждения от большинства узлов реплика-сета, но не более 5 секунд; при таймауте операция завершится с ошибкой, хотя запись могла быть выполнена частично.


Чтение — find и findOne

Метод findOne(filter?, projection?, options?) возвращает один документ, соответствующий фильтру (первый в порядке сканирования), или null, если совпадений нет. Он не возвращает курсор — результат сразу материализуется. Этот метод удобен для поиска по уникальному полю (включая _id).

Метод find(filter?, projection?, options?) возвращает курсор — объект, позволяющий лениво итерироваться по результатам запроса. Курсор извлекает данные пачками (batch size регулируется на стороне сервера и клиента). Это критически важно при работе с большими выборками: приложение получает данные по мере необходимости, не расходуя избыточную память.

<коллекция>.findOne(<фильтр>[, <проекция>][, <опции>])
<коллекция>.find(<фильтр>[, <проекция>][, <опции>])

Важно понимать, что find() без фильтра возвращает все документы коллекции. Это может привести к значительной нагрузке на сеть и клиент, если коллекция содержит миллионы записей. Всегда применяйте ограничения (limit, skip, sort) или фильтры в production-сценариях.


Обновление — updateOne, updateMany, replaceOne

MongoDB различает модификационные и заменяющие операции.

  • updateOne(filter, update, options?) и updateMany(filter, update, options?) применяют операторы обновления ($set, $inc, $push и др.) к существующим документам. Обновляется только указанные поля; остальная структура документа сохраняется. Поведение по умолчанию: если фильтр не находит ни одного документа — операция "тихо" завершается без изменений.

  • replaceOne(filter, replacement, options?) полностью заменяет найденный документ новым объектом (кроме _id, который сохраняется, если не указан иначе). Все поля оригинала, отсутствующие в replacement, теряются.

<коллекция>.updateOne(<фильтр>, <операторы_обновления>[, <опции>])
<коллекция>.updateMany(<фильтр>, <операторы_обновления>[, <опции>])
<коллекция>.replaceOne(<фильтр>, <новый_документ>[, <опции>])

Идемпотентная вставка по бизнес-ключу (без устаревшего save):

db.products.updateOne(
{ sku: "BOOK-01" },
{ $setOnInsert: { title: "Вводный курс", price: 1200 } },
{ upsert: true }
);

Ключевая опция — upsert: true. При её включении, если фильтр не находит совпадений, MongoDB вставляет новый документ. При этом:

  • Если в update используется $setOnInsert, его поля применяются только при вставке;
  • Поле _id при вставке генерируется автоматически, если не указано явно в фильтре или в replacement/update.

Другие важные опции:

  • arrayFilters — позволяет адресовать конкретные элементы массива по условию, например:
    updateMany({}, { $set: { "grades.$[g]": 100 } }, { arrayFilters: [ { g: { $gte: 90 } } ] }).
  • hint — явное указание индекса для выполнения обновления.

Операторы обновления:

{ $set: { "<поле>": <новое_значение> } }
{ $inc: { "<поле>": <число> } }
{ $push: { "<массив>": <элемент> } }
{ $unset: { "<поле>": "" } }
{ $setOnInsert: { "<поле>": <значение> } }

Удаление — deleteOne, deleteMany

Методы deleteOne(filter, options?) и deleteMany(filter, options?) удаляют документы, соответствующие фильтру. Удаление не возвращает сами документы — только счётчик (deletedCount). Для получения удалённого документа следует использовать findOneAndDelete(filter, options?), который атомарно находит, возвращает и удаляет один документ.

<коллекция>.deleteOne(<фильтр>[, <опции>])
<коллекция>.deleteMany(<фильтр>[, <опции>])
<коллекция>.findOneAndDelete(<фильтр>[, <опции>])

Очистка данных и удаление коллекции — разные операции:

db.orders.deleteMany({}) // данные удалены, индексы/TTL/валидаторы остаются
db.orders.drop() // коллекция и метаданные уничтожены

deleteMany({}) не освобождает диск сразу (фоновая компактизация WiredTiger); drop() убирает структуру целиком.


Построение запросов

Запросы в MongoDB строятся с помощью BSON-объектов, содержащих операторы запроса — специальные ключи, начинающиеся с символа $.


Операторы сравнения

  • $eq, $ne — равно / не равно (явно указываются, если требуется семантика "точного совпадения", включая тип);
  • $gt, $gte, $lt, $lte — строгое и нестрогое сравнение;
  • $in, $nin — проверка вхождения значения в массив (или отсутствия в нём).
    Пример: { status: { $in: ["active", "pending"] } }.

Важно: сравнение значений разных типов подчиняется правилам BSON-сортировки: null < числа < строки < объекты < массивы < бинарные данные < ObjectId < булевы < даты < регулярные выражения < MinKey < MaxKey. Это влияет как на сортировку, так и на результаты $gt/$lt.


Логические операторы

  • $and — конъюнкция (по умолчанию используется при перечислении полей — {a — 1, b: 2} эквивалентно { $and: [{a: 1}, {b: 2}] });
  • $or — дизъюнкция;
  • $not — логическое отрицание (применяется к одному условию: { name: { $not: { $eq: "admin" } } });
  • $nor — отрицание дизъюнкции ({ $nor — [{a — 1}, {b: 1}] } ⇔ ни a=1, ни b=1).

Сложные запросы строятся рекурсивно. Например, выборка документов, где (age < 18 или age > 65) и status ≠ "blocked":

{
$and: [
{ $or: [ { age: { $lt: 18 } }, { age: { $gt: 65 } } ] },
{ status: { $ne: "blocked" } }
]
}

Операторы элементов и массивов

  • $exists: true|false — проверка наличия поля;
  • $type: "<typeName>" | <typeNumber> — проверка типа значения (например, "string" или 2);
  • $all — все указанные элементы должны присутствовать в массиве (независимо от порядка);
  • $size — точное совпадение размера массива (не поддерживает индексацию — требует сканирования);
  • $elemMatch — условие применяется к одному и тому же элементу вложенного массива объектов.
    Пример: найти студентов, у которых есть оценка ≥85 по предмету "math":
    { grades: { $elemMatch: { subject: "math", score: { $gte: 85 } } } }.

Фильтр

{ "<поле>": <значение> }
{ "<поле>": { $gt: <порог> } }
{ "<вложенный.путь>": <значение> }
{ $and: [ <условие1>, <условие2> ] }
{ $or: [ <условие1>, <условие2> ] }

Проекция

{ "<поле>": 1 } // включить поле
{ "<поле>": 0 } // исключить поле
{ "_id": 0, "<поле>": 1 } // исключить _id, включить другое

Опции операций

{ upsert: true }
{ ordered: false }
{ writeConcern: { w: "majority", wtimeout: <мс> } }
{ arrayFilters: [ { "<идентификатор>": <условие> } ] }
{ hint: "<имя_индекса>" }

Пример составной операции

<коллекция>.updateMany(
{ "оценки": { $elemMatch: { "предмет": "математика" } } },
{ $set: { "оценки.$[элемент].балл": 5 } },
{ arrayFilters: [ { "элемент.балл": { $lt: 5 } } ] }
)

Курсоры

Как уже отмечалось, find() возвращает курсор — объект, поддерживающий цепочку методов для последовательной обработки результата.

  • .sort(sortSpec) — задаёт порядок сортировки: { field: 1 } — по возрастанию, { field: -1 } — по убыванию. Можно сортировать по нескольким полям. Важно: сортировка без подходящего индекса может потребовать сортировки в памяти (ограничение по умолчанию — 32 МБ; при превышении — ошибка, если не указан allowDiskUse: true).

  • .limit(n) — ограничивает количество возвращаемых документов. Эффективно используется вместе с sort, чтобы получить топ-N результатов.

  • .skip(n) — пропускает первые n документов. Часто применяется для пагинации, но неэффективно при больших смещениях (skip(10000).limit(10) требует сканирования 10 010 документов). Для глубокой пагинации рекомендуется курсорная пагинация на основе последнего значения сортируемого поля.

  • .projection({ field — 1 | 0 }) — управляет возвращаемыми полями — 1 — включить, 0 — исключить (нельзя смешивать в одном запросе, кроме как для _id). Исключение тяжёлых полей (например, payload, logs) значительно снижает объём передаваемых данных.

  • .hint(indexName) — явное указание индекса, что полезно при отладке или неоднозначных планах.

  • .explain("executionStats") — возвращает детали выполнения запроса: использованный индекс, число просканированных и возвращённых документов (totalDocsExamined vs nReturned), время выполнения. Ключевой инструмент для оптимизации.

Курсоры имеют ограниченное время жизни на сервере (обычно 10 минут неактивности). При превышении — ошибка cursor not found. В прикладном коде следует избегать длительных пауз между hasNext() и next().


Индексы

Индекс в MongoDB — это отдельная структура данных (B-дерево или его вариации), которая хранит отсортированные значения одного или нескольких полей и ссылки на соответствующие документы. Индексы позволяют избежать полного сканирования коллекции (COLLSCAN) и переходить к нужным документам напрямую (IXSCAN).


Основные типы индексов

  • Однопольный индекс ({ field: 1 }) — самый простой и часто используемый.
  • Составной индекс ({ a — 1, b — -1 }) — эффективен для запросов, фильтрующих по a, или по a и b, или сортирующих по (a, b). Порядок полей критичен: префикс индекса должен совпадать с фильтром.
  • Уникальный индекс ({ email: 1 }, { unique: true }) — гарантирует отсутствие дубликатов по указанному полю(ям). Ошибка при вставке/обновлении вызывает исключение E11000.
  • Частичный индекс ({ status: "active" }, { partialFilterExpression: { status: "active" } }) — индексирует только документы, удовлетворяющие условию. Экономит место и ускоряет обновления для редко запрашиваемых подмножеств.
  • TTL-индекс ({ createdAt — 1 }, { expireAfterSeconds: 86400 }) — автоматически удаляет документы, возраст которых превышает указанное число секунд. Используется для логов, сессий, временных задач.
  • Текстовый индекс ({ title: "text", body: "text" }) — поддерживает полнотекстовый поиск с ранжированием по релевантности ({ $meta: "textScore" }).
  • Геопространственные индексы (2dsphere, 2d) — для запросов вида "найти точки в радиусе", "ближайшие объекты".

Принципы эффективного индексирования

  1. Покрывающие индексы — когда все запрашиваемые поля присутствуют в индексе. MongoDB может выполнить запрос, не обращаясь к самим документам (FETCH stage отсутствует).
  2. Выборочность — индекс эффективен, если отсекает значительную часть данных. Индекс по полю gender (2 значения) почти бесполезен при фильтре gender: "M", если половина коллекции — мужчины.
  3. Накладные расходы — каждый индекс замедляет вставку, обновление и удаление (т.к. нужно обновлять и структуру индекса). Оптимальное число индексов — 3–7 на коллекцию в большинстве сценариев.
  4. Комбинирование фильтрации, сортировки и проекции — индекс может обслуживать все три этапа одновременно, если его структура соответствует порядку — равенство → сортировка → диапазон (правило ESR — Equality, Sort, Range).

Анализ эффективности индексов:

db.orders.find({ status: "open" }).explain("executionStats")
db.orders.stats()
db.orders.totalIndexSize()

Агрегация данных

Общая база вызова в коде — функции в коде. Операторы $sum, $avg, $function и оконные стадии — вычисления на стороне БД.

Агрегация в MongoDB — это механизм преобразования, фильтрации, группировки и вычисления данных непосредственно на стороне сервера. В отличие от клиентской обработки результата find(), агрегация минимизирует объём передаваемых данных и использует оптимизированные внутренние алгоритмы, что критично для производительности при работе с миллионами записей.

Основная абстракция — конвейер (pipeline): последовательность этапов (stages), через которые проходят документы. Каждый этап принимает поток документов, применяет к ним преобразование и передаёт результат следующему этапу. Конвейер задаётся массивом BSON-объектов, где каждый объект соответствует одному этапу и имеет ровно одно поле с ключом, начинающимся с $.


Базовые этапы конвейера

  • $match — фильтрация документов по условию, аналогичному параметру find().
{ $match: { status: "active", createdAt: { $gte: ISODate("2025-01-01") } } }

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

  • $project — переопределение структуры документа — включение, исключение, вычисление новых полей.
{ $project: {
name: 1,
emailDomain: { $split: ["$email", "@"] },
isAdult: { $gte: ["$age", 18] }
} }

Поддерживает арифметические, строковые, логические, датовые и условные операторы ($add, $substr, $cond, $dateToString, $size, $type и др.). Можно использовать $project для "разворачивания" вложенных объектов ($mergeObjects) или упрощения структуры перед $group.

  • $group — агрегация документов по указанному ключу (или _id: null для глобальной группировки) с применением аккумуляторных операторов.
{ $group: {
_id: "$department",
avgSalary: { $avg: "$salary" },
totalEmployees: { $sum: 1 },
maxSalary: { $max: "$salary" },
employees: { $push: "$name" } // собирает все имена в массив
} }

Поддерживаемые аккумуляторы — $sum, $avg, $min, $max, $first, $last, $push, $addToSet, $stdDevPop, $stdDevSamp.

  • $sort — упорядочивание документов. Работает идентично .sort() у курсора, но в рамках конвейера может потребовать allowDiskUse: true, если объём данных превышает лимит памяти (100 МБ по умолчанию).
    Часто используется перед $limit для выборки топ-N.

  • $skip и $limit — пагинация результатов. Как и при работе с курсорами, skip неэффективен при больших смещениях; для глубокой пагинации предпочтителен паттерн "range-based pagination" с $match по последнему известному значению сортируемого поля.


Продвинутые этапы

  • $unwind — развёртывание массива: на каждый элемент массива создаётся отдельный документ. Удобен для анализа вложенных коллекций.
{ $unwind: "$tags" } // { _id: 1, tags: ["A", "B"] } → два документа: { _id: 1, tags: "A" }, { _id: 1, tags: "B" }

Опция preserveNullAndEmptyArrays — true сохраняет документы, где поле отсутствует, null или пустой массив.

  • $lookup — выполнение операции, аналогичной left outer join в SQL. Позволяет объединять данные из разных коллекций без денормализации.
{ $lookup: {
from: "orders",
localField: "_id",
foreignField: "customerId",
as: "orders"
} }

Используется с осторожностью: при отсутствии индекса по foreignField может привести к полному сканированию целевой коллекции на каждый документ.

  • $facet — одновременный запуск нескольких независимых подконвейеров и объединение их результатов в один документ. Применяется для построения сложных отчётов: например, агрегация + распределение по интервалам + топ-10 — всё в одном запросе.
{ $facet: {
summary: [ { $group: { _id: null, total: { $sum: 1 } } } ],
byStatus: [ { $group: { _id: "$status", count: { $sum: 1 } } } ],
topAuthors: [ { $sort: { views: -1 } }, { $limit: 5 } ]
} }
  • $bucket и $bucketAuto — автоматическое распределение документов по диапазонам (bucketing). Полезно для построения гистограмм.
{ $bucket: {
groupBy: "$price",
boundaries: [0, 100, 500, 1000, Infinity],
default: "other",
output: { count: { $sum: 1 } }
} }
  • $setWindowFields (начиная с MongoDB 5.0) — оконные функции — вычисление скользящих средних, рангов, накопительных сумм без группировки.
{ $setWindowFields: {
partitionBy: "$department",
sortBy: { salary: -1 },
output: {
rank: { $rank: {} },
runningTotal: { $sum: "$salary", window: { documents: ["unbounded", "current"] } }
}
} }

Конвейер оптимизируется автоматически — MongoDB переставляет этапы, чтобы максимально использовать индексы (например, sort + limit может быть заменён на "top-k" операцию с индексом), убирает избыточные project, объединяет последовательные $match.

Для отладки агрегации:

db.orders.aggregate([{ $match: { status: "open" } }, { $group: { _id: "$dept", n: { $sum: 1 } } }])
.explain("executionStats")

Транзакции

До версии 4.0 MongoDB поддерживала только одно-документные атомарные операции. С выходом 4.0 появилась поддержка много-документных транзакций с гарантиями ACID (Atomicity, Consistency, Isolation, Durability), сначала для реплика-сетов, а с 4.2 — и для шардированных кластеров.

Транзакции реализованы на уровне сессии — объекта, представляющего клиентский контекст. Сессия создаётся явно (client.startSession()) или неявно (при использовании withTransaction() в драйверах). Все операции в рамках транзакции выполняются в одной сессии.

Пример (синтаксис mongosh, псевдокод):

const session = db.getMongo().startSession();
session.startTransaction();
try {
db.accounts.updateOne({ _id: "A" }, { $inc: { balance: -100 } }, { session });
db.accounts.updateOne({ _id: "B" }, { $inc: { balance: +100 } }, { session });
session.commitTransaction();
} catch (error) {
session.abortTransaction();
throw error;
} finally {
session.endSession();
}

Ключевые ограничения и особенности

  • Время жизни: транзакция автоматически прерывается, если не завершена в течение 60 секунд (по умолчанию; настраивается через transactionLifetimeLimitSeconds). Продление невозможно.
  • Размер и объём: суммарный размер изменений в одной транзакции — не более 16 МБ; число изменённых документов — не более 1000 (актуальные лимиты см. в документации). При превышении — ошибка.
  • Совместимость с операциями — не поддерживаются операции, изменяющие схему (createCollection, drop, createIndex), а также distinct, mapReduce, geoNear, text search внутри транзакции.
  • Уровни изоляции: MongoDB использует snapshot isolation — транзакция работает с согласованным снимком данных на момент её начала. Читает не видят неподтверждённых записей других транзакций; писатели не блокируют читателей (MVCC на базе WiredTiger).
  • Производительность: транзакции накладывают издержки — логирование в transaction table, управление снапшотами. Их следует использовать только там, где требуется строгая согласованность (денежные переводы, инвентаризация), а не "на всякий случай".

Для большинства сценариев (например, обновление профиля пользователя) достаточно одно-документной атомарности (updateOne с $set, $inc, $push и т.д.), так как документ сам по себе — естественная граница согласованности.


Практическое задание

CLI-вариант того же цикла (insert → find → index → aggregate) без Compass: Первые шаги с MongoDB.

Цель — ознакомиться с графическим интерфейсом, визуально исследовать структуру данных, выполнить CRUD-операции и проанализировать производительность запросов.


Шаг 1. Установка

  1. Перейдите на официальную страницу загрузок: https://www.mongodb.com/try/download/compass
  2. Выберите версию, соответствующую вашей ОС (Windows, macOS, Linux).
  3. Запустите установщик и следуйте инструкциям (на Linux — распакуйте архив и запустите бинарный файл).

Примечание — Compass не требует предварительной установки MongoDB Server, но для работы ему нужен запущенный mongod (локальный или удалённый, например, Atlas).


Шаг 2. Подключение

  1. Откройте Compass.
  2. В поле Connection String введите URI:
mongodb://localhost:27017
mongodb+srv://USER:PASSWORD@cluster0.xxx.mongodb.net/
  1. Нажмите Connect.

Шаг 3. Исследование базы данных

  1. После подключения вы увидите список баз данных слева.
  2. Выберите базу sample_mflix (если используете Atlas) или создайте свою в mongosh:
use my_app
db.createCollection("movies")
  1. Перейдите в коллекцию movies.
  2. Вкладка Documents показывает первые 20 документов. Используйте фильтр вверху (Filter) для поиска:
{ "year": { "$gte": 2000 }, "imdb.rating": { "$gte": 8.5 } }
  1. Нажмите Explain Plan, чтобы увидеть, какой индекс использован, сколько документов просканировано.

Шаг 4. Создание и обновление данных

  1. Нажмите Insert DocumentInsert Document.
  2. Вставьте JSON:
{
"title": "Практическое задание",
"year": 2025,
"genres": ["учеба", "IT"],
"plot": "Первая запись через Compass"
}
  1. Нажмите Insert.
  2. Найдите этот документ по title, откройте его и нажмите Edit Document. Добавьте поле completed: true. Сохраните.

Шаг 5. Агрегация через Compass

  1. Перейдите во вкладку Aggregations.
  2. Нажмите Add Stage → выберите $match:
{ "year": { "$gte": 2000 } }
  1. Добавьте $group:
{
"_id": "$genres",
"count": { "$sum": 1 }
}
  1. Нажмите Run, чтобы увидеть распределение фильмов по жанрам.
  2. Добавьте $sort: { "count": -1 } — результат отсортируется по убыванию.

Шаг 6. Анализ и индексы

  1. Перейдите во вкладку Indexes.
  2. Нажмите Create Index.
  3. Укажите поле year — 1, поставьте галочку Unique (снимите, если значения не уникальны), нажмите Create Index.
  4. Вернитесь в Documents, выполните прежний фильтр — в Explain Plan ожидайте стадию IXSCAN (индекс), а не COLLSCAN (полный скан коллекции).

Это задание даёт практическое понимание связи между структурой данных, запросами, индексами и интерфейсом.


Сравнение с реляционными СУБД

MongoDB и классические реляционные СУБД (PostgreSQL, MySQL, SQL Server) решают схожие задачи — хранение, обработка и обеспечение целостности данных, — но делают это с разными акцентами. Отсутствие JOIN’ов, отсутствие строгой схемы и отсутствие табличных нормальных форм в MongoDB — осознанные архитектурные решения, оптимизированные под определённые рабочие нагрузки.

В реляционных системах данные нормализуются для устранения дублирования и обеспечения целостности. Это требует множественных JOIN’ов при чтении, но упрощает обновление: изменение в одном месте распространяется автоматически. В MongoDB предпочтение отдаётся денормализации и вложению — ради уменьшения числа чтений и повышения скорости ответа. Это увеличивает объём хранимых данных и усложняет обновление, но критически важно для высоконагруженных веб- и мобильных приложений, где время отклика — ключевой метрик.

Выбор между моделями следует делать по следующим критериям:

  • Изменчивость схемы: если структура данных постоянно эволюционирует в процессе разработки или эксплуатации — документная модель снижает издержки на миграции.
  • Сложность связей — если домен характеризуется глубокой иерархией (например, заказ → позиции → детали → свойства), вложенные документы отражают её естественно. Если связи плоские и равноправные (например, "пациент — врач — приём — диагноз"), реляционная модель может быть проще.
  • Требования к согласованности — если бизнес-логика требует строгих транзакций между множеством сущностей (банковские переводы, бухгалтерия), реляционные СУБД исторически сильнее. Однако с поддержкой много-документных транзакций MongoDB закрыла этот разрыв для большинства прикладных сценариев.
  • Масштабируемость "по горизонтали": MongoDB изначально проектировалась для шардирования. Добавление новых узлов и перераспределение данных — штатная операция. В реляционных системах горизонтальное масштабирование требует значительных усилий (Citus, Vitess, шардирование на уровне приложения).

На практике многие современные системы используют гибридный подход — основной поток событий и операционные данные — в MongoDB, а аналитика, отчётность и регуляторные отчёты — в реляционных или OLAP-хранилищах (через CDC, Change Streams, ETL).


Типовые сценарии применения MongoDB

Социальные сети и контент-платформы

Посты, комментарии, лайки, профили пользователей — естественно моделируются как вложенные или связанные документы. Например, пост может содержать массив комментариев, каждый из которых — объект с автором, временем, текстом и, возможно, вложенными ответами. Обновление счётчиков (число лайков) выполняется атомарно через $inc. Для поиска по тегам или тексту используется текстовый индекс или интеграция с Atlas Search.


Каталоги товаров и электронная коммерция

Продукты часто имеют неоднородные атрибуты — у ноутбука — процессор, ОЗУ, видеокарта; у книги — автор, ISBN, жанр. В MongoDB каждый товар может храниться как единый документ с полем specifications, содержащим произвольный набор пар "ключ-значение". Фильтрация по цене, рейтингу, наличию — через индексы. Рекомендации строятся на основе агрегации истории просмотров и покупок.


IoT и временные ряды

Сенсоры генерируют потоки данных с отметкой времени. MongoDB поддерживает коллекции временных рядов (начиная с 5.0) — оптимизированный тип коллекции, где документы группируются по meta (идентификатор устройства) и time, обеспечивая эффективное хранение и запросы по диапазонам времени. Агрегация позволяет строить скользящие средние, детектировать аномалии ($setWindowFields, $stdDevSamp).


Логирование и аналитика событий

Каждое событие (запрос API, клик, ошибка) записывается как документ. Благодаря гибкости схемы, события разных типов могут храниться в одной коллекции с разным набором полей. TTL-индексы автоматически удаляют старые записи. Агрегация позволяет строить dashboards в реальном времени — DAU/MAU, funnel-анализ, топ-10 медленных запросов.


Генеративный ИИ и векторный поиск

MongoDB Atlas предоставляет векторные индексы (на основе HNSW), позволяя хранить и искать эмбеддинги (векторные представления текста, изображений) в той же коллекции, что и операционные данные. Это устраняет необходимость синхронизации между operational и vector-базами. Семантический поиск, рекомендации, RAG (Retrieval-Augmented Generation) реализуются через этап $vectorSearch в агрегационном конвейере — без выгрузки данных во внешние сервисы.


Проектирование схемы

Сначала запросы, потом документы

В реляционной модели часто начинают с нормализованной ER-диаграммы. В MongoDB практичнее сначала зафиксировать шаблоны доступа — какие запросы выполняются чаще всего, что читается и пишется вместе, какая кардинальность связей (1:1, 1:N, N:M, "один к миллионам"). Схема подстраивается под эти запросы — данные, нужные одним find, стремятся оказаться в одном документе; редко используемые поля выносят в отдельную коллекцию.

MongoDB не навязывает единственный способ моделирования связей. Разработчик выбирает между:

  • Вложением (embedding) — дочерние документы хранятся внутри родительского.
    Преимущества — атомарная запись, одиночное чтение, естественное отражение иерархии.
    Ограничения: размер документа ≤ 16 МБ; обновление вложенных элементов требует $ или $[]. Индекс по полю-массиву создаёт multikey-индекс (отдельная запись на каждый элемент); для произвольных вложенных путей без явного списка полей — wildcard-индекс { "$**": 1 }.

  • Ссылками (referencing) — хранение _id связанного документа как поля.
    Преимущества: отсутствие ограничений по размеру; независимое управление жизненным циклом; поддержка циклических связей.
    Недостатки: требуется несколько запросов или $lookup для извлечения связанных данных; отсутствие атомарности между коллекциями без транзакции.

    Ручная ссылка — обычный способ: в документе лежит идентификатор связанной записи (ObjectId, UUID или осмысленный строковый ключ):

db.companies.insertOne({ _id: "acme", name: "Acme Corp", founded: 1998 });
db.users.insertOne({ name: "Alex", age: 30, companyId: "acme" });

const user = db.users.findOne({ name: "Alex" });
db.companies.findOne({ _id: user.companyId });

Целостность "как у внешнего ключа" обеспечивает приложение (проверки перед удалением, транзакции). Устаревшие форматы DBRef ({ $ref, $id, $db }) и тип DBPointer сегодня не используют: драйвер не подставляет связанный документ сам, проще явное поле companyId и при необходимости $lookup.

  • Гибридным подходом — частичное вложение "горячих" данных (например, имя и аватар автора поста), а остальное — по ссылке. Это баланс между производительностью и гибкостью.

Рекомендации по выбору:

  • Вкладывайте, если связь "один-ко-многим" и дочерние объекты всегда запрашиваются вместе с родителем (комментарии под постом, пункты заказа).
  • Ссылайтесь, если связь "многие-ко-многим" или дочерние объекты используются автономно (пользователи и роли, товары и категории).
  • Используйте частичное вложение для denormalization часто читаемых, редко изменяемых атрибутов (например, authorName в посте, даже если полный профиль — отдельный документ).

MongoDB также поддерживает валидацию схемы на уровне коллекции через JSON Schema. Это позволяет внедрить частичную строгость — например, требовать наличие поля email, проверять его формат регулярным выражением, задавать enum для status. Валидация применяется при вставке и обновлении, но не мешает добавлять опциональные расширения — гибкость сохраняется.

Шаблоны проектирования схем

Повторяющиеся задачи моделирования удобно решать именованными шаблонами (см. таблицу ниже и MongoDB — проектирование документной схемы):

ШаблонКогда применять
ПолиморфныйОдна коллекция, документы похожи, но с разным набором полей (типы товаров, события разных классов)
Attribute (EAV)Много "динамических" атрибутов; пары ключ/значение в массиве + один индекс вместо сотни колонок
Bucket (ведро)Временные ряды, IoT: точки за час/день в одном документе (массив readings), а не документ на каждую секунду
Outlier (выброс)Редкие "тяжёлые" документы (миллион подписчиков у одного пользователя) — флаг + overflow-коллекция по ссылке _id
ComputedДорогие агрегаты при каждом чтении — фоновый пересчёт полей в документе
Subset (подмножество)Рабочий набор не помещается в RAM: "горячие" N отзывов в основной коллекции, архив — отдельно
Extended referenceЗаказ дублирует имя/адрес клиента из другой коллекции, чтобы собрать экран одним запросом
ApproximationСчётчики просмотров/лайков — обновление каждые N событий вместо каждого
Tree (материализованный путь)Каталог: массив ancestors + поле прямой категории для индекса по иерархии
Document versioning"Текущая" версия в основной коллекции, история — в *_history

Нормализация (отдельные коллекции + $lookup) ускоряет запись и уменьшает дублирование; денормализация (вложение/копии полей) ускоряет чтение одним запросом. Для связи "многие ко многим" с миллионами рёбер (друзья, подписчики) вложение массива id в документ пользователя быстро упирается в 16 МБ и стоимость обновлений — обычно отдельная коллекция рёбер с индексами по обоим концам.

Развёрнутый конспект с примерами bucket, extended reference, outlier и миграциями — MongoDB — проектирование документной схемы.


Отсутствие поля, null и типы в фильтрах

В отличие от SQL, "пустое значение" разбивается на два случая:

СитуацияКак искать
Поля нет в документе{ status: { $exists: false } }
Поле есть и равно null{ status: null }

Запросы строго различают типы: { age: 28 } и { age: "28" } — разные выборки. В shell голые числа часто становятся Double; для целых задавайте NumberInt / NumberLong, для денег — NumberDecimal.


GridFS

Один BSON-документ ограничен 16 МБ. Крупные вложения (PDF, видео, дампы) для хранения внутри MongoDB режутся механизмом GridFS: файл разбивается на чанки (по умолчанию 255 КиБ) в двух служебных коллекциях:

КоллекцияСодержимое
fs.filesимя, размер, дата, произвольные metadata
fs.chunksбинарные фрагменты, привязка к files_id

Чтение идёт потоком чанков, без загрузки всего файла в память. Имя bucket по умолчанию — fs (коллекции fs.files и fs.chunks); можно задать своё, например attachments.files / attachments.chunks.

Из shell (Database Tools):

mongofiles --db=company_db put ./report.pdf
mongofiles --db=company_db list
mongofiles --db=company_db get report.pdf
mongofiles --db=company_db delete report.pdf

В mongosh (драйвер в оболочке):

Код ITЗагрузка примера кода…

В прикладном документе обычно хранят fileId (из attachments.files) или имя файла, а не сам бинарник.

Уместно: вложения в той же БД, что и метаданные; файлы от мегабайт до сотен мегабайт; прототип без отдельного хранилища объектов.

Лучше object storage: публичные медиа, CDN, архивы на терабайты — в MongoDB оставляют только URI и атрибуты; GridFS нагружает операционный кластер и не заменяет CDN.


Когда MongoDB — слабый выбор

MongoDB силён там, где документ отражает экран или агрегат приложения. Стоит пересмотреть выбор СУБД, если доминирует:

  • Сложные JOIN и нормализованная отчётность на том же кластере, что и OLTP (лучше PostgreSQL / warehouse + CDC).
  • Жёсткие междокументные инварианты на каждую операцию без готовности к multi-document transactions и их лимитам.
  • Частые cross-shard транзакции в шардированном кластере — дорого и хрупко по сравнению с моделью "один шард — одна сущность".
  • Аналитика тяжёлых агрегатов на primary без выноса в $out, Change Streams или внешний OLAP.

Multi-document транзакции (replica set) — страховка, а основной инструмент согласованности — атомарность одного документа и продуманная схема. См. §9 транзакций.


Антипаттерны и типичные ошибки

  • Хранение больших бинарных объектов в BinData — файлы (изображения, видео) размером более нескольких мегабайт следует хранить в объектных хранилищах (S3, MinIO), а в MongoDB оставлять только метаданные и URL. Для файлов до 16 МБ можно использовать GridFS — специализированный механизм, разбивающий файл на чанки.

  • Чрезмерная вложенность — вложенные объекты глубже 3–4 уровней усложняют запросы (field.subfield.subsubfield.value), затрудняют индексацию и увеличивают риск превышения лимита в 16 МБ. При необходимости — плоская структура с составными ключами.

  • Отсутствие TTL для временных данных — логи, сессии, кэши должны автоматически удаляться. Без TTL-индекса накопление данных приведёт к росту дискового пространства и замедлению операций.

  • Использование skip для глубокой пагинацииskip(100000) требует сканирования 100 000 документов. Альтернатива — курсорная пагинация по индексируемому полю (например, createdAt), где клиент передаёт последнее значение как маркер.

// Плохо при больших offset:
db.orders.find().sort({ _id: 1 }).skip(10000).limit(20);

// Лучше — "ключ следующей страницы":
db.orders.find({ _id: { $gt: lastSeenId } }).sort({ _id: 1 }).limit(20);
  • Создание индексов "на всякий случай" — каждый индекс замедляет запись. Регулярно анализируйте использование индексов через indexStats и удаляйте неиспользуемые.

  • Попытка имитировать JOIN’ы через вложенные $lookup в каждом запросе — если связанные данные нужны постоянно, лучше денормализовать. $lookup — для редких отчётов, а не для hot-path.


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.