MongoDB: полное руководство для разработчика
MongoDB — это документоориентированная NoSQL база данных, которая хранит данные в формате BSON (бинарный JSON). В отличие от реляционных баз, MongoDB не требует фиксированной схемы и позволяет хранить данные в гибкой, иерархической структуре.
В этой статье мы разберём CRUD операции, агрегации, индексы, репликацию, шардирование и лучшие практики использования MongoDB в продакшене.
Установка и запуск
# Установка через apt (Debian/Ubuntu)
wget -qO - https://www.mongodb.org/static/pgp/server-7.0.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
sudo apt update
sudo apt install -y mongodb-org
# Установка через Homebrew (macOS)
brew tap mongodb/brew
brew install mongodb-community
# Запуск сервера
sudo systemctl start mongod
# или
mongod --config /etc/mongod.conf
# Запуск клиента
mongosh
Проверка подключения:
mongosh "mongodb://localhost:27017"
# test> db.runCommand({ ping: 1 })
# { ok: 1 }
Основные понятия
Документ — основная единица хранения, аналог строки в реляционных БД. Записывается в формате BSON.
{
"_id": ObjectId("654f8a2b1c9d440012345678"),
"name": "Alice",
"email": "alice@example.com",
"age": 30,
"tags": ["python", "mongodb"],
"address": {
"city": "Moscow",
"street": "Tverskaya",
"building": "1"
}
}
Коллекция — группа документов, аналог таблицы. Коллекции не требуют фиксированной схемы.
База данных — контейнер для коллекций. Один сервер MongoDB может хранить множество баз данных.
_id — уникальный идентификатор документа. Если не указан, MongoDB автоматически генерирует ObjectId.
CRUD операции
Create (Создание)
// Вставка одного документа
db.users.insertOne({
name: "Bob",
email: "bob@example.com",
age: 25,
createdAt: new Date()
})
// Вставка нескольких документов
db.users.insertMany([
{ name: "Charlie", email: "charlie@example.com", age: 35 },
{ name: "Diana", email: "diana@example.com", age: 28 },
{ name: "Eve", email: "eve@example.com", age: 32 }
])
// Вставка с явным _id
db.users.insertOne({
_id: "user_001",
name: "Frank",
email: "frank@example.com"
})
Python с PyMongo:
from pymongo import MongoClient
from datetime import datetime
client = MongoClient("mongodb://localhost:27017/")
db = client.myapp
users = db.users
# Вставка одного документа
result = users.insert_one({
"name": "Alice",
"email": "alice@example.com",
"age": 30,
"created_at": datetime.utcnow()
})
print(f"Inserted ID: {result.inserted_id}")
# Вставка нескольких документов
results = users.insert_many([
{"name": "Bob", "email": "bob@example.com", "age": 25},
{"name": "Charlie", "email": "charlie@example.com", "age": 35}
])
print(f"Inserted IDs: {results.inserted_ids}")
Read (Чтение)
// Найти все документы
db.users.find()
// Найти с фильтром
db.users.find({ age: 30 })
// Найти один документ
db.users.findOne({ email: "alice@example.com" })
// Операторы сравнения
db.users.find({ age: { $gt: 25 } }) // > 25
db.users.find({ age: { $gte: 25 } }) // >= 25
db.users.find({ age: { $lt: 30 } }) // < 30
db.users.find({ age: { $lte: 30 } }) // <= 30
db.users.find({ age: { $ne: 30 } }) // != 30
db.users.find({ age: { $in: [25, 30, 35] } })
db.users.find({ age: { $nin: [20, 40] } })
// Логические операторы
db.users.find({
$and: [
{ age: { $gt: 25 } },
{ age: { $lt: 35 } }
]
})
db.users.find({
$or: [
{ age: 25 },
{ age: 30 }
]
})
db.users.find({
age: { $not: { $lt: 30 } }
})
// Работа с вложенными полями
db.users.find({ "address.city": "Moscow" })
// Работа с массивами
db.users.find({ tags: "python" }) // Содержит "python"
db.users.find({ tags: { $all: ["python", "mongodb"] } })
db.users.find({ tags: { $size: 3 } }) // Ровно 3 элемента
// Регулярные выражения
db.users.find({ name: { $regex: /^A/i } }) // Начинается на A
// Проекция (выбор полей)
db.users.find(
{ age: { $gt: 25 } },
{ name: 1, email: 1, _id: 0 } // Вернуть name, email, без _id
)
// Сортировка
db.users.find().sort({ age: 1 }) // По возрастанию
db.users.find().sort({ age: -1 }) // По убыванию
// Лимит и пропуск
db.users.find().limit(10)
db.users.find().skip(20).limit(10) // Пагинация
// Подсчёт документов
db.users.countDocuments({ age: { $gt: 25 } })
db.users.estimatedDocumentCount() // Быстрая приблизительная оценка
Python примеры:
from pymongo import ASCENDING, DESCENDING
# Найти все документы
for user in users.find():
print(user)
# Найти с фильтром
user = users.find_one({ "email": "alice@example.com" })
# Операторы
adult_users = users.find({ "age": { "$gte": 18 } })
# Сортировка и лимит
top_users = users.find().sort("age", DESCENDING).limit(5)
# Проекция
names_only = users.find({}, { "name": 1, "_id": 0 })
# Подсчёт
count = users.count_documents({ "age": { "$gt": 25 } })
Update (Обновление)
// Обновление одного документа
db.users.updateOne(
{ email: "alice@example.com" },
{ $set: { age: 31 } }
)
// Обновление нескольких документов
db.users.updateMany(
{ age: { $lt: 18 } },
{ $set: { isMinor: true } }
)
// Операторы обновления
$set // Установить значение
$unset // Удалить поле
$inc // Инкремент
$mul // Умножение
$min // Минимум (если меньше текущего)
$max // Максимум (если больше текущего)
$rename // Переименовать поле
// Примеры
db.users.updateOne(
{ _id: 1 },
{
$set: { name: "Alice Updated" },
$inc: { age: 1 },
$unset: { tempField: "" }
}
)
// Обновление массивов
$push // Добавить элемент
$pop // Удалить первый/последний элемент
$pull // Удалить по значению
$addToSet // Добавить, если нет (уникальность)
$each // Несколько элементов
$slice // Ограничить размер массива
db.users.updateOne(
{ _id: 1 },
{
$push: { tags: "django" },
$addToSet: { tags: "python" }, // Не добавит, если уже есть
$pull: { tags: "old_tag" }
}
)
// Обновление вложенных документов
db.users.updateOne(
{ _id: 1 },
{ $set: { "address.city": "Saint Petersburg" } }
)
// Замена всего документа
db.users.replaceOne(
{ _id: 1 },
{ name: "New Name", email: "new@example.com" }
// _id сохраняется, остальные поля заменяются
)
// Upsert (обновление или вставка)
db.users.updateOne(
{ email: "newuser@example.com" },
{ $set: { name: "New User", age: 25 } },
{ upsert: true } // Создаст, если не найден
)
Python примеры:
# Обновление одного документа
result = users.update_one(
{ "email": "alice@example.com" },
{ "$set": { "age": 31 } }
)
print(f"Matched: {result.matched_count}, Modified: {result.modified_count}")
# Обновление нескольких
result = users.update_many(
{ "age": { "$lt": 18 } },
{ "$set": { "isMinor": True } }
)
# Upsert
result = users.update_one(
{ "email": "newuser@example.com" },
{ "$set": { "name": "New User", "age": 25 } },
{ "upsert": True }
)
print(f"Upserted ID: {result.upserted_id}")
# Обновление массива
users.update_one(
{ "_id": 1 },
{ "$push": { "tags": "django" } }
)
Delete (Удаление)
// Удаление одного документа
db.users.deleteOne({ email: "alice@example.com" })
// Удаление нескольких документов
db.users.deleteMany({ age: { $lt: 18 } })
// Удаление всех документов (осторожно!)
db.users.deleteMany({})
// Проверка перед удалением
db.users.countDocuments({ age: { $lt: 18 } })
Python:
# Удаление одного
result = users.delete_one({ "email": "alice@example.com" })
print(f"Deleted: {result.deleted_count}")
# Удаление нескольких
result = users.delete_many({ "age": { "$lt": 18 } })
print(f"Deleted: {result.deleted_count}")
Агрегации
Агрегации позволяют обрабатывать данные через конвейер (pipeline).
Основные этапы конвейера
// $match - фильтрация (как find)
// $project - трансформация документов
// $group - группировка
// $sort - сортировка
// $limit - ограничение
// $skip - пропуск
// $lookup - JOIN с другой коллекцией
// $unwind - развернуть массив
// $addFields - добавить вычисляемые поля
// Пример: статистика по возрастам
db.users.aggregate([
{
$match: { age: { $gte: 18 } }
},
{
$group: {
_id: "$age",
count: { $sum: 1 },
avgSalary: { $avg: "$salary" }
}
},
{
$sort: { count: -1 }
},
{
$limit: 10
}
])
// Пример: группировка по диапазонам
db.users.aggregate([
{
$group: {
_id: {
$switch: {
branches: [
{ case: { $lt: ["$age", 18] }, then: "minor" },
{ case: { $lt: ["$age", 30] }, then: "young" },
{ case: { $lt: ["$age", 50] }, then: "middle" }
],
default: "senior"
}
},
count: { $sum: 1 }
}
}
])
// $lookup - LEFT JOIN
db.orders.aggregate([
{
$lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "user"
}
},
{ $unwind: "$user" } // Преобразовать массив в документ
])
// $unwind - развернуть массив
db.posts.aggregate([
{ $unwind: "$tags" },
{
$group: {
_id: "$tags",
count: { $sum: 1 }
}
}
])
// Вычисляемые поля
db.products.aggregate([
{
$addFields: {
discountedPrice: { $multiply: ["$price", 0.9] },
fullName: { $concat: ["$firstName", " ", "$lastName"] },
createdAt: { $toDate: "$_id" } // Из ObjectId
}
}
])
// Кондициональная логика
db.users.aggregate([
{
$addFields: {
status: {
$cond: {
if: { $gte: ["$age", 18] },
then: "adult",
else: "minor"
}
}
}
}
])
Python примеры:
# Простая агрегация
pipeline = [
{ "$match": { "age": { "$gte": 18 } } },
{ "$group": {
"_id": "$age",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } }
]
results = users.aggregate(pipeline)
# С lookup (JOIN)
pipeline = [
{ "$lookup": {
"from": "orders",
"localField": "_id",
"foreignField": "userId",
"as": "orders"
}},
{ "$addFields": {
"totalOrders": { "$size": "$orders" }
}}
]
results = users.aggregate(pipeline)
Агрегатные функции
$sum // Сумма
$avg // Среднее
$min // Минимум
$max // Максимум
$push // Массив значений
$addToSet // Уникальный массив
$first // Первое значение
$last // Последнее значение
$count // Количество
$stdDevPop // Стандартное отклонение
$stdDevSamp // Стандартное отклонение (выборка)
Индексы
Индексы ускоряют чтение, но замедляют запись.
Типы индексов
// Одиночный индекс
db.users.createIndex({ email: 1 }) // 1 = ascending, -1 = descending
// Составной индекс
db.users.createIndex({ lastName: 1, firstName: 1 })
// Мультиключевой индекс (для массивов)
db.posts.createIndex({ tags: 1 })
// Текстовый индекс
db.articles.createIndex({ title: "text", content: "text" })
// Поиск по тексту
db.articles.find({
$text: { $search: "python mongodb" }
})
// Геоспространственный индекс
db.places.createIndex({ location: "2dsphere" })
// Поиск рядом
db.places.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [37.6176, 55.7558] },
$maxDistance: 5000 // метров
}
}
})
// Уникальный индекс
db.users.createIndex({ email: 1 }, { unique: true })
// Частичный индекс (только для документов с условием)
db.users.createIndex(
{ email: 1 },
{ partialFilterExpression: { isActive: true } }
)
// TTL индекс (автоудаление по времени)
db.sessions.createIndex(
{ expiresAt: 1 },
{ expireAfterSeconds: 3600 }
)
Управление индексами:
// Показать все индексы
db.users.getIndexes()
// Удалить индекс
db.users.dropIndex("email_1")
// Удалить все кроме _id
db.users.dropIndexes()
// Перестроить индекс
db.users.reIndex()
// Анализ использования индексов
db.users.aggregate([{ $indexStats: {} }])
Python:
# Создание индекса
users.create_index([("email", ASCENDING)], unique=True)
users.create_index([("lastName", ASCENDING), ("firstName", ASCENDING)])
# Текстовый индекс
users.create_index([("bio", "text")])
# Частичный индекс
users.create_index(
[("email", ASCENDING)],
partialFilterExpression={"isActive": True}
)
Explain (Анализ запросов)
// Анализ плана выполнения
db.users.find({ email: "alice@example.com" }).explain("executionStats")
// Вывод:
// - winningPlan: выбранный план
// - executionStats: статистика выполнения
// - nReturned: количество возвращённых документов
// - totalDocsExamined: сколько документов просмотрено
// - indexName: использованный индекс
Репликация
Репликация обеспечивает отказоустойчивость и чтение с реплик.
Replica Set
Минимум 3 ноды: 1 primary + 2 secondary.
Запуск реплика-сета (локально):
# Терминал 1
mkdir -p /data/rs0-0
mongod --replSet rs0 --port 27017 --dbpath /data/rs0-0 --bind_ip localhost
# Терминал 2
mkdir -p /data/rs0-1
mongod --port 27018 --dbpath /data/rs0-1 --bind_ip localhost
# Терминал 3
mkdir -p /data/rs0-2
mongod --port 27019 --dbpath /data/rs0-2 --bind_ip localhost
Инициализация:
// Подключение к первой ноде
mongosh --port 27017
// Инициализация реплика-сета
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "localhost:27017", priority: 2 },
{ _id: 1, host: "localhost:27018", priority: 1 },
{ _id: 2, host: "localhost:27019", priority: 1 }
]
})
// Проверка статуса
rs.status()
// Проверка конфигурации
rs.conf()
// Добавление члена
rs.add("localhost:27020")
// Удаление члена
rs.remove("localhost:27020")
// Принудительный failover
rs.stepDown()
Подключение с реплика-сетом:
from pymongo import MongoClient, ReadPreference
# Подключение к реплика-сету
client = MongoClient(
"mongodb://localhost:27017,localhost:27018,localhost:27019/?replicaSet=rs0"
)
# Чтение с secondary (для снижения нагрузки на primary)
client = MongoClient(
"mongodb://localhost:27017,localhost:27018,localhost:27019/?replicaSet=rs0&readPreference=secondaryPreferred"
)
Шардирование
Шардирование — горизонтальное масштабирование через разделение данных.
Компоненты кластера
Shard — хранит часть данных (реплика-сет).
Config Server — хранит метаданные кластера (реплика-сет).
Mongos — роутер запросов (stateless).
Настройка шардирования
# Запуск config server
mongod --configsvr --replSet csReplSet --port 27019 --dbpath /data/config
# Запуск shard
mongod --shardsvr --replSet shard01 --port 27018 --dbpath /data/shard01
# Запуск mongos
mongos --configdb csReplSet/localhost:27019 --port 27017
Инициализация:
// Подключение к mongos
mongosh --port 27017
// Добавление шарда
sh.addShard("shard01/localhost:27018")
// Включение шардирования для базы
sh.enableSharding("myapp")
// Шардирование коллекции
sh.shardCollection("myapp.users", { userId: "hashed" })
// Составной шардированный ключ
sh.shardCollection("myapp.orders", { customerId: 1, orderId: 1 })
// Зонирование (привязка данных к шардам)
sh.addTagRange(
"myapp.users",
{ region: "EU" },
{ region: "EU~" },
"shard-eu"
)
Выбор шардированного ключа
Хороший ключ:
- Высокая кардинальность (много уникальных значений)
- Равномерное распределение записей
- Частые запросы используют ключ
Плохой ключ:
- Монотонно возрастающий (timestamp, _id)
- Низкая кардинальность (gender, status)
Транзакции
MongoDB поддерживает многодокументные транзакции (с версии 4.0).
const session = db.getMongo().startSession();
session.startTransaction();
try {
db.accounts.updateOne(
{ _id: "acc1" },
{ $inc: { balance: -100 } },
{ session }
);
db.accounts.updateOne(
{ _id: "acc2" },
{ $inc: { balance: 100 } },
{ session }
);
session.commitTransaction();
} catch (error) {
session.abortTransaction();
throw error;
} finally {
session.endSession();
}
Python:
from pymongo import MongoClient
client = MongoClient()
with client.start_session() as session:
with session.start_transaction():
client.db.accounts.update_one(
{"_id": "acc1"},
{"$inc": {"balance": -100}},
session=session
)
client.db.accounts.update_one(
{"_id": "acc2"},
{"$inc": {"balance": 100}},
session=session
)
Best Practices
Схема данных
Встраивание vs Ссылки:
// Встраивание (для данных "один ко многим", читаемых вместе)
{
_id: "order123",
customer: {
name: "Alice",
email: "alice@example.com"
},
items: [
{ product: "Laptop", qty: 1, price: 1000 },
{ product: "Mouse", qty: 2, price: 50 }
]
}
// Ссылки (для больших данных или частых обновлений)
{
_id: "order123",
customerId: "user456",
itemIds: ["item1", "item2", "item3"]
}
Валидация схемы
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "email", "age"],
properties: {
name: { bsonType: "string" },
email: {
bsonType: "string",
pattern: "^.+@.+\\..+$"
},
age: { bsonType: "int", minimum: 0 }
}
}
}
})
Оптимизация запросов
// Используйте проекцию
db.users.find({}, { name: 1, email: 1 })
// Избегайте $where и $function
// Вместо $where: { $where: "this.age > 25" }
// Используйте: { age: { $gt: 25 } }
// Ограничивайте результат
db.users.find().limit(100)
// Используйте покрывающие индексы
// Индекс покрывает все поля запроса и проекции
Мониторинг
// Текущие операции
db.currentOp()
// Медленные запросы
db.setProfileLevel(1, { slowms: 100 })
db.system.profile.find().sort({ ts: -1 }).limit(5)
// Статистика базы
db.stats()
// Статистика коллекции
db.users.stats()
// Размер коллекции
db.users.totalSize()
Резервное копирование
# mongodump
mongodump --out /backup/mongodb
# mongorestore
mongorestore /backup/mongodb
# С конкретной базы
mongodump --db myapp --out /backup
# С конкретной коллекции
mongodump --db myapp --collection users --out /backup
# Восстановление
mongorestore --db myapp /backup/myapp/
Заключение
MongoDB — это гибкая, масштабируемая база данных для современных приложений:
- Гибкая схема — быстрая итерация разработки
- Мощные агрегации — сложная аналитика без дополнительных инструментов
- Горизонтальное масштабирование — шардирование для больших объёмов
- Отказоустойчивость — репликация и автоматический failover
- Богатый язык — поддержка сложных запросов и индексов
Используйте MongoDB для проектов с быстро меняющейся схемой, большими объёмами данных и высокими требованиями к масштабируемости.