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

MongoDB — самая популярная NoSQL база данных в мире. Она хранит данные в формате документов (JSON/BSON), не требует жёсткой схемы и масштабируется горизонтально.

Уровень: начальный 19 разделов ⏱ 3–4 часа

1. Что такое MongoDB

MongoDB — это документо-ориентированная NoSQL база данных. Вместо строк и столбцов она хранит данные в виде документов в формате BSON (бинарный JSON).

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

КонцепцияSQLMongoDB
Хранилище данныхТаблицаКоллекция
Единица данныхСтрока (row)Документ (document)
СхемаЖёсткая (ALTER TABLE)Гибкая (каждый документ может отличаться)
Язык запросовSQLMongoDB Query API
СвязиJOINВложенность или $lookup

Пример документа:

{
  "_id": ObjectId("64f1a2b3c4d5e6f7a8b9c0d1"),
  "name": "Иван Петров",
  "age": 28,
  "email": "ivan@example.com",
  "tags": ["python", "mongodb"],
  "address": {
    "city": "Москва",
    "street": "Ленина 10"
  }
}

Когда использовать MongoDB

2. Установка и запуск

Скачай MongoDB Community Server с mongodb.com и установи. По умолчанию сервер слушает порт 27017.

Запуск сервера

mongod --dbpath /data/db

В отдельном терминале подключись через клиент mongosh:

mongosh

Проверь подключение:

db.runCommand({ ping: 1 })
{ ok: 1 }

GUI — MongoDB Compass

Скачай Compass с mongodb.com/products/compass. Подключись по адресу mongodb://localhost:27017.

Запуск через Docker

docker run -d --name mongo -p 27017:27017 mongo:7
Для продакшена используй Docker Compose с постоянным хранилищем: добавьте -v mongo-data:/data/db.

3. Основные понятия

ТерминОписание
База данныхКонтейнер для коллекций. Создаётся автоматически при первом обращении.
КоллекцияГруппа документов — аналог таблицы в SQL.
ДокументЗапись в коллекции. Формат BSON (JSON-подобный).
_idУникальный идентификатор документа. Тип ObjectId по умолчанию.

ObjectId — это 12-байтный ключ: 4 байта временной метки + 5 байт рандома + 3 байта счётчика. Он гарантирует уникальность без централизованного генератора.

ObjectId("64f1a2b3c4d5e6f7a8b9c0d1")
    ↑ 4 байта          ↑ 5 байт     ↑ 3 байта
    Timestamp          Random       Counter

Документы могут содержать вложенность:

{
  "title": "iPhone 15",
  "price": 89990,
  "specs": {
    "screen": "6.1-inch",
    "chip": "A17 Pro"
  },
  "colors": ["black", "white", "blue"]
}
У MongoDB нет обязательной схемы. Два документа в одной коллекции могут иметь разные поля — но это не значит, что схема не нужна. В продакшене используйте валидацию.

4. Вставка документов

insertOne

db.users.insertOne({
  name: "Иван Петров",
  age: 28,
  email: "ivan@example.com",
  orders: [
    { product: "Ноутбук", price: 75000, date: ISODate("2025-01-15") },
    { product: "Мышь", price: 1500, date: ISODate("2025-02-20") }
  ]
})

Ответ:

{
  acknowledged: true,
  insertedId: ObjectId("64f1a2b3c4d5e6f7a8b9c0d1")
}

insertMany

db.users.insertMany([
  {
    name: "Мария Сидорова",
    age: 34,
    email: "maria@example.com",
    tags: ["admin", "editor"]
  },
  {
    name: "Алексей Козлов",
    age: 22,
    email: "alex@example.com",
    tags: ["viewer"]
  }
])
insertMany по умолчанию выполняется атомарно. Если один документ не проходит вставку (например, нарушение уникального индекса), вся пакетная вставка отменяется.

5. Запросы — find

Получение всех документов

db.users.find()

Фильтрация

db.users.find({ age: { $gt: 30 } })
db.users.find({ "tags": "admin" })
db.users.find({ age: { $gte: 25, $lte: 35 } })

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

db.users.find({
  $or: [
    { age: { $gt: 30 } },
    { tags: "admin" }
  ]
})

db.users.find({
  $and: [
    { age: { $gte: 20 } },
    { tags: "viewer" }
  ]
})

Проекция

Второй аргумент find — проекция. Укажи 1 для включения или 0 для исключения:

db.users.find(
  { age: { $gt: 25 } },
  { name: 1, email: 1, _id: 0 }
)

Форматированный вывод

db.users.find().pretty()

6. Обновление документов

updateOne и updateMany

db.users.updateOne(
  { name: "Иван Петров" },
  { $set: { age: 29, "address.city": "Санкт-Петербург" } }
)

db.users.updateMany(
  { tags: "viewer" },
  { $inc: { age: 1 } }
)

Другие операторы обновления

ОператорОписаниеПример
$setУстановить значение{ $set: { status: "active" } }
$incУвеличить на число{ $inc: { views: 1 } }
$pushДобавить в массив{ $push: { tags: "new" } }
$pullУбрать из массива{ $pull: { tags: "old" } }
$unsetУдалить поле{ $unset: "temp" }

findOneAndUpdate

db.users.findOneAndUpdate(
  { name: "Иван Петров" },
  { $set: { lastLogin: ISODate() } },
  { returnDocument: "after" }
)

Upsert

Параметр upsert: true создаёт документ, если он не найден:

db.users.updateOne(
  { email: "new@example.com" },
  { $set: { name: "Новый", age: 25 } },
  { upsert: true }
)

7. Удаление документов

deleteOne

db.users.deleteOne({ name: "Алексей Козлов" })

deleteMany

db.users.deleteMany({ age: { $lt: 18 } })

Очистка всей коллекции

db.users.deleteMany({})
db.users.drop()
drop() удаляет коллекцию целиком вместе со всеми индексами. deleteMany({}) очищает документы, но коллекция остаётся.

8. Агрегация

Aggregation Pipeline — цепочка этапов обработки данных. Каждый этап получает результат предыдущего.

Основные этапы

ЭтапОписание
$matchФильтрация документов (аналог find)
$groupГруппировка по полю с агрегатными функциями
$sortСортировка результатов
$limitОграничение количества документов
$projectВыбор и переименование полей

Пример: средний возраст по городам

db.users.aggregate([
  { $match: { age: { $gte: 18 } } },
  {
    $group: {
      _id: "$address.city",
      avgAge: { $avg: "$age" },
      totalUsers: { $sum: 1 }
    }
  },
  { $sort: { avgAge: -1 } }
])

Результат:

[
  { _id: "Москва", avgAge: 31, totalUsers: 150 },
  { _id: "Санкт-Петербург", avgAge: 29, totalUsers: 80 }
]

Пример: топ-5 самых дорогих заказов

db.orders.aggregate([
  { $unwind: "$items" },
  { $group: {
      _id: "$userId",
      totalSpent: { $sum: "$items.price" }
    }
  },
  { $sort: { totalSpent: -1 } },
  { $limit: 5 }
])

9. Индексы и производительность

Индексы ускоряют поиск. Без индекса MongoDB выполняет коллекционное сканирование (full collection scan).

Создание индекса

db.users.createIndex({ email: 1 }, { unique: true })
db.users.createIndex({ age: 1, "address.city": 1 })

Значение 1 — восходящий индекс, -1 — нисходящий.

Проверка выполнения запроса

db.users.find({ email: "ivan@example.com" }).explain("executionStats")

В выводе обрати внимание на totalDocsExamined — с индексом это число мало, без индекса равно размеру коллекции.

TTL-индексы

TTL-индексы автоматически удаляют документы через указанное время. Идеальны для кэша и сессий:

db.sessions.createIndex(
  { createdAt: 1 },
  { expireAfterSeconds: 3600 }
)

Документы с полем createdAt старше 1 часа будут удалены автоматически.

Не создавайте индексы на все подряд поля. Каждый индекс замедляет запись. Создавайте индексы только для полей, по которым часто ищете или сортируете.

10. Работа из Python

Установка pymongo

pip install pymongo

Подключение и CRUD

from pymongo import MongoClient

client = MongoClient("mongodb://localhost:27017")
db = client["myshop"]
users = db["users"]

users.insert_one({
    "name": "Иван",
    "age": 28,
    "email": "ivan@example.com"
})

user = users.find_one({"name": "Иван"})
print(user["email"])

for u in users.find({"age": {"$gte": 25}}):
    print(u["name"], u["age"])

users.update_one({"name": "Иван"}, {"$set": {"age": 29}})
users.delete_one({"name": "Иван"})
pymongo работает с Python-словарями напрямую. MongoDB возвращает документы как dict, поэтому обращайтесь к полям через user["name"] или user.get("name").

Motor — асинхронный клиент

pip install motor
import asyncio
from motor.motor_asyncio import AsyncIOMotorClient

client = AsyncIOMotorClient("mongodb://localhost:27017")
db = client["myshop"]

async def main():
    await db.users.insert_one({"name": "Пётр", "age": 30})
    user = await db.users.find_one({"name": "Пётр"})
    print(user)

asyncio.run(main())

11. MongoDB Atlas и продакшен

MongoDB Atlas — облачная управляемая версия MongoDB. Бесплатный тариф включает 512 МБ хранилища.

Подключение из приложения

from pymongo import MongoClient

client = MongoClient(
    "mongodb+srv://user:password@cluster0.abc123.mongodb.net/"
    "?retryWrites=true&w=majority"
)
db = client["production"]

Бэкапы и безопасность

Вставка в REST-приложение

from fastapi import FastAPI
from pymongo import MongoClient

app = FastAPI()
client = MongoClient("mongodb://localhost:27017")
db = client["api"]

@app.post("/users")
def create_user(name: str, age: int):
    result = db.users.insert_one({"name": name, "age": age})
    return {"id": str(result.inserted_id)}

@app.get("/users/{user_id}")
def get_user(user_id: str):
    from bson import ObjectId
    user = db.users.find_one({"_id": ObjectId(user_id)})
    if user:
        user["_id"] = str(user["_id"])
        return user
    return {"error": "not found"}

12. Транзакции и ACID

В отличие от ранних версий, современный MongoDB (начиная с 4.0) поддерживает многодокументные транзакции с гарантиями ACID. Транзакция работает только на реплика-сетах и позволяет гарантировать согласованность нескольких записей одновременно.

const session = client.startSession();
session.startTransaction();
try {
  await db.orders.insertOne(order, { session });
  await db.accounts.updateOne(
    { _id: userId },
    { $inc: { balance: -order.total } },
    { session }
  );
  await session.commitTransaction();
} catch (err) {
  await session.abortTransaction();
  throw err;
} finally {
  session.endSession();
}
Транзакция не заменяет грамотную схему. Если поля обновляются независимо, используйте $set с гарантией идемпотентности, а тяжёлые многошаговые операции — через транзакции и компенсирующие действия.

Таймаут транзакции по умолчанию — 60 секунд (transactionLifetimeLimitSeconds). Держите транзакции короткими: читайте данные до транзакции, а внутри меняйте только то, что критично для атомарности.

13. Репликация и отказоустойчивость

Реплика-сет (replica set) — это группа серверов MongoDB, хранящих одинаковый набор данных. Один узел — primary (принимает записи), остальные — secondary (копии данных). Если primary выходит из строя, набор автоматически выбирает нового primary за несколько секунд.

rs.initiate({
  _id: "rs0",
  members: [
    { _id: 0, host: "10.0.0.1:27017" },
    { _id: 1, host: "10.0.0.2:27017" },
    { _id: 2, host: "10.0.0.3:27017" }
  ]
})
Один сервер — не реплика-сет. Хотя команды репликации выполнятся и на одиночном узле (standalone), отказоустойчивости при этом нет. Нужен минимум три узла по принципу большинства (quorum).

14. Роли и безопасность доступа

По умолчанию MongoDB слушает localhost без пароля. Для продакшена обязательно включайте авторизацию (--auth или security.authorization) и создавайте отдельного пользователя на каждое приложение с минимумом прав.

РольПрава
readчтение документов из коллекций базы
readWriteчтение, вставка, обновление и удаление
dbAdminадминистрирование: индексы, валидация
userAdminуправление пользователями базы
clusterAdminполный доступ к кластеру и репликации
use admin
db.createUser({
  user: "app_user",
  pwd: passwordPrompt(),
  roles: ["readWrite"]
})
TLS/SSL, IP-whitelist и шифрование диска (encryption at rest) закрывают большинство векторов атак. Не выставляйте порт 27017 наружу без необходимости — используйте приватную сеть или SSH-туннель.

15. Бэкапы и восстановление

Утилита mongodump сохраняет данные в BSON-файлы, а mongorestore восстанавливает их. Для горячих копий реплика-сета предпочтителен непрерывный бэкап через Ops Manager или Atlas — там снимки делаются автоматически.

mongodump --uri="mongodb://localhost:27017" \
  --db=shop --out=/backups/shop_2026-09-19

mongorestore --uri="mongodb://localhost:27017" \
  --db=shop /backups/shop_2026-09-19/shop
Экспорт через mongodump не бесплатен: на больших объёмах он занимает время и память, а сам бэкап не гарантирует мгновенную точку восстановления (point-in-time). Для продакшена с высоким RPO используйте репликацию и снапшоты облачных дисков.

Регулярно проверяйте восстановление: бэкап, который никогда не пробовали развернуть, — это просто файлы на диске. Расписание: cron раз в сутки плюс еженедельная проверка целостности.

16. Change Streams: реакция на изменения

Change Streams дают приложению подписку на изменения в коллекции, базе или кластере: вставки, обновления и удаления приходят в режиме реального времени. Это аналог SQL-триггеров, но на стороне сервиса — реакция на события остаётся в коде, а не в базе.

const pipeline = [
  { $match: { operationType: 'insert' } }
];
const stream = db.catalog.watch(pipeline);

stream.on('change', (change) => {
  const doc = change.fullDocument;
  console.log('Новый товар:', doc._id);
});

Поток возобновим: сохраняйте поле resumeToken события и при перезапуске продолжайте чтение с последней позиции. Потери событий при этом исключены — в отличие от периодических опросов коллекции.

СценарийПример
Единый стримингсинхронизация данных в поисковую систему
Событийная архитектурауведомления, планировщики, аналитика
Материализованные представленияподдержание агрегатов на лету
Change Streams работают только на replica set и на standalone-инсталляциях с включённым oplog. На отдельном сервере без реплики подписка не заработает.

17. Геопространственные запросы и GeoJSON

MongoDB умеет хранить геометрию в формате GeoJSON (точки, линии, полигоны) и отвечать на вопросы «что рядом», «что внутри района», «какие маршруты пересекаются». Для скорости создайте индекс 2dsphere по геополю.

db.places.createIndex({ location: '2dsphere' });

db.places.find({
  location: {
    $near: {
      $geometry: { type: 'Point', coordinates: [37.62, 55.75] },
      $maxDistance: 1000
    }
  }
});
ОператорНазначение
$nearобъекты рядом, отсортированные по расстоянию
$geoWithinобъекты внутри полигона или круга
$geoIntersectsгеометрии, пересекающие заданную область
Координаты GeoJSON передаются строго в порядке «долгота, широта». Перепутанный порядок — самая частая ошибка: результат просто не совпадёт с ожидаемым местом на карте.

18. Полнотекстовый поиск и Atlas Search

Встроенный текстовый индекс text позволяет искать по словам и фразам, учитывая язык и стоп-слова. Достаточно создать его по одному или нескольким полям, а запрос написать через $text.

db.articles.createIndex(
  { title: 'text', body: 'text' },
  { default_language: 'russian' }
);

db.articles.find(
  { $text: { $search: 'питон базы данных' } },
  { score: { $meta: 'textScore' } }
).sort({ score: { $meta: 'textScore' } });

Оператор $meta: 'textScore' добавляет в документ релевантность совпадения, по которой сортируются результаты. Фразы заключайте в кавычки, а минус перед словом исключает его из поиска.

Для серьёзных систем (опечатки, синонимы, фасеты) лучше поднять отдельный поисковый движок, например Elasticsearch. Встроенный $text покрывает базовые кейсы без дополнительной инфраструктуры.

19. Шардинг: горизонтальное масштабирование

Шардинг распределяет данные между несколькими реплика-сетами по ключу шардирования. Запросы идут через маршрутизатор mongos, который знает, где хранится каждый фрагмент данных. Это даёт горизонтальное масштабирование, когда один сервер уже не справляется.

sh.enableSharding('catalog');
sh.shardCollection(
  'catalog.products',
  { category: 1, _id: 1 }
);
СтратегияПоведениеПрименение
hashedравномерное распределение по хэшу ключазаписи без диапазонных запросов
rangedдиапазоны значений (даты, числа)отчёты и срезы по интервалу
Выбор ключа шардирования — ответственное решение, изменить его потом сложно. Уникальность _id сохраняется только при включении ключа в состав индексов, а неравномерный ключ создаёт «горячие» шарды.

20. Валидация схем: JSON Schema

В отличие от SQL, MongoDB по умолчанию не проверяет структуру документа — новое поле может появиться в любой момент. На продакшене это превращает «небрежность» в тихую катастрофу аналитики, поэтому включите валидацию: документу, не прошедшему правила, вставка будет запрещена или заменена.

db.runCommand({
  collMod: 'users',
  validator: {
    $jsonSchema: {
      bsonType: 'object',
      required: ['email', 'role'],
      properties: {
        email: { bsonType: 'string',
                 pattern: '@' },
        role: { enum: ['admin', 'user'] }
      }
    }
  },
  validationLevel: 'strict'
});
УровеньПоведение
strictпроверяет новые и обновляемые документы
moderateпроверяет только новые вставки
offвалидация отключена
Валидация не заменяет уникальность: правило «один email» всё равно делается через createIndex с опцией unique.

21. TTL-индексы: данные с истечением срока

TTL-индекс по полю с датой автоматически удаляет документы, чьё время вышло: сессии, логи и коды подтверждения не копятся на диске, а чистятся фоновым процессом примерно раз в минуту. Избавьтесь от cron-джобов, удаляющих строки вручную, — база справится сама.

db.sessions.createIndex(
  { createdAt: 1 },
  { expireAfterSeconds: 3600 }
);
Пересчитывайте срок просто: 604800 — неделя, 2592000 — месяц.
TTL подходит для «пассивных» данных. Нужна реакция на истечение — свяжите удаление с Change Streams и обрабатывайте событие сами.

22. Курсоры и пагинация без пропусков

Метод find возвращает не все документы сразу, а курсор — окно с ленивой подгрузкой партий. Привычные limit/skip на больших страницах тормозят: серверу приходится заново проходить предыдущие документы. Надёжнее курсор по ключу: следующий запрос начинается с _id последней записи.

let lastId = null;
for (let page = 0; page < 20; page++) {
  const q = lastId ? { _id: { $gt: lastId } } : {};
  const batch = await db.posts.find(q)
    .sort({ _id: 1 }).limit(100).toArray();
  if (batch.length === 0) break;
  lastId = batch[batch.length - 1]._id;
}

Такой подход стабилен, даже если во время листания добавляются новые записи: страницы не «плавают». Чтобы курсор не сканировал весь диск, поле сортировки должно совпадать с индексом — как правило, хватает _id.

countDocuments() с фильтром проходит все документы и на большой коллекции медленный. Для оценок используйте estimatedDocumentCount(): он читает метаданные, а не данные.

23. Паттерн Outbox: события без потерь

Когда MongoDB — источник правды, а изменения должна увидеть поисковая система или очередь, наивное «обновил базу, потом отправил событие» теряет события при падении между двумя шагами. Паттерн outbox кладёт сам документ и запись о событии в одну транзакцию: изменятся либо оба, либо ни один.

const session = db.startSession();
session.startTransaction();
await db.orders.insertOne(order, { session });
await db.outbox.insertOne({
  type: 'order.created',
  payload: order._id,
  status: 'pending'
}, { session });
await session.commitTransaction();

Фоновый релейер читает pending-записи, публикует их в RabbitMQ, Kafka или пуш-сервис и помечает обработанными. Получатель обязан быть идемпотентным: повторная доставка не должна задваивать заказы — учитывайте идентификатор события.

Без строгой транзакции события можно дедуплицировать по _id в отдельной коллекции — дешевле, но допускает редкие дубли.
Не публикуйте события синхронно из запроса пользователя: внешний сервис может быть недоступен, и операция повиснет вместе с ответом клиенту.

Что дальше

Теперь ты знаешь основы MongoDB. Рекомендуемый следующий шаг — изучение Redis для кэширования, или углубление в продвинутые темы: Docker для контейнеризации, FastAPI для построения REST API с MongoDB.

← Вернуться к курсам