Документы и коллекции
Что такое документ
Документ — BSON-объект, аналог строки в SQL.
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"email": "alice@example.com",
"name": "Алиса",
"age": 30,
"tags": ["vip", "premium"],
"address": {
"city": "Москва",
"street": "Ленина, 5"
},
"created_at": ISODate("2026-10-15T10:00:00Z")
}
Ключевое:
- Вложенные объекты.
- Массивы.
- Разные типы.
- Не требуется одинаковый набор полей для всех документов коллекции.
Ограничения документа
- 16 MB — максимум документа.
_id— обязательное поле, уникальное.- Имена полей — не начинаются с
$, не содержат.и\0. - Глубина вложенности — обычно до 100 уровней.
Что делать при >16 MB:
- GridFS — разбивает файлы на чанки.
- Реструктуризация — вынести часть в другую коллекцию.
Коллекция
Аналог таблицы, но без схемы.
- Создаётся неявно при первой вставке.
- Или явно —
db.createCollection(). - Валидация — опционально (
$jsonSchema).
db.users.insertOne({ name: 'Алиса' }) // коллекция создана
db.createCollection('products') // явно
Типы данных BSON
Основные:
string— UTF-8.int32,int64— целые.double— 8 байт.decimal128— точные числа (деньги).bool— true / false.date— timestamp (мс).null.array— массив любых значений.object— вложенный документ.ObjectId— 12 байт.binData— бинарные данные.regex— регулярка.
Специфичные:
Decimal128— точные дроби.Timestamp— внутренний, для oplog.MinKey/MaxKey— сортировочные крайности.
Вставка
Один:
db.users.insertOne({
email: 'alice@example.com',
name: 'Алиса',
age: 30,
})
Сколько:
const result = db.users.insertOne({ ... })
result.insertedId // ObjectId
Много:
db.users.insertMany([
{ email: 'a@b.com', name: 'Алиса' },
{ email: 'c@d.com', name: 'Боб' },
], { ordered: false })
ordered: false — продолжать даже при ошибке (например, дубликат _id).
С указанием _id:
db.users.insertOne({
_id: new ObjectId(),
email: 'x@y.com'
})
Правило: не задавать _id вручную без причины — ObjectId генерируется автоматически.
Чтение
Все:
db.users.find()
Один:
db.users.findOne({ email: 'alice@example.com' })
По _id:
db.users.findOne({ _id: ObjectId('507f1f77bcf86cd799439011') })
Проекция полей:
db.users.find({}, { email: 1, name: 1, _id: 0 })
// _id всегда включается, если не убрать явно
Лимит и скип:
db.users.find().limit(10).skip(20)
Сортировка:
db.users.find().sort({ created_at: -1 })
// 1 — ascending, -1 — descending
Обновление
Один:
db.users.updateOne(
{ _id: userId },
{ $set: { name: 'Новое имя' } }
)
Несколько:
db.users.updateMany(
{ age: { $lt: 18 } },
{ $set: { is_minor: true } }
)
Upsert:
db.users.updateOne(
{ email: 'a@b.com' },
{ $set: { name: 'Алиса' } },
{ upsert: true }
)
Что даёт: insert, если не найдено; update, если найдено.
Операторы обновления
$set — установить значение:
{ $set: { 'address.city': 'Питер' } } // вложенное поле
$unset — удалить поле:
{ $unset: { legacy_field: '' } }
$inc — увеличить:
{ $inc: { login_count: 1 } }
{ $inc: { balance: -100 } }
$push — добавить в массив:
{ $push: { tags: 'new_tag' } }
{ $push: { tags: { $each: ['a', 'b'] } } }
$pull — удалить из массива:
{ $pull: { tags: 'old_tag' } }
$addToSet — добавить, если нет:
{ $addToSet: { tags: 'php' } }
$pop — удалить с конца (-1) или начала (1):
{ $pop: { tags: 1 } }
$rename — переименовать:
{ $rename: { 'old_name': 'new_name' } }
$currentDate — текущая дата:
{ $currentDate: { updated_at: true } }
$min / $max — только если меньше/больше:
{ $min: { min_price: 100 } }
$mul — умножить:
{ $mul: { price: 1.1 } }
Массивы
Добавить:
{ $push: { tags: 'php' } }
{ $push: { tags: { $each: ['php', 'laravel'], $position: 0 } } }
{ $push: { tags: { $each: ['a', 'b'], $slice: 5 } } } // максимум 5
{ $push: { tags: { $each: ['a', 'b'], $sort: 1 } } } // сортировать
Удалить:
{ $pull: { tags: 'php' } }
{ $pull: { tags: { $in: ['php', 'js'] } } }
Заменить элемент по индексу:
{ $set: { 'tags.0': 'ruby' } }
Обновить все элементы:
{ $set: { 'scores.$[]': 0 } }
Обновить элементы по условию:
{ $set: { 'scores.$[elem]': 0 } }
// с arrayFilters:
{ arrayFilters: [{ 'elem': { $lt: 0 } }] }
Удаление
Один:
db.users.deleteOne({ _id: userId })
Много:
db.users.deleteMany({ is_inactive: true })
Очистить коллекцию:
db.users.deleteMany({})
⚠️ deleteMany({}) удаляет всё, но не коллекцию.
_id
По умолчанию: ObjectId.
Можно свой тип:
db.users.insertOne({ _id: 'user-123', name: 'X' })
db.users.insertOne({ _id: 42, name: 'Y' })
⚠️ _id неизменяем. После вставки — только удалить документ.
Правило: для обычных случаев — ObjectId. Для ссылок из внешних систем — свой _id (slug, external_id).
Вложенные документы
Доступ через точку:
db.users.updateOne(
{ _id: userId },
{ $set: { 'address.city': 'Питер' } }
)
db.users.find({ 'address.city': 'Питер' })
Когда вкладывать:
- Всегда читается вместе с родителем.
- 1:1 или 1:мало.
- Не нужен поиск по вложенному в других контекстах.
Когда отдельная коллекция:
- Растёт неограниченно (иначе упрёшься в 16 MB).
- Часто обновляется отдельно.
- Нужен поиск и пагинация.
Пример: заказ
{
"_id": ObjectId("..."),
"order_number": "ORD-2026-00001",
"user_id": ObjectId("..."),
"items": [
{
"product_id": ObjectId("..."),
"name": "iPhone 15",
"price": NumberDecimal("99990.00"),
"quantity": 1
}
],
"subtotal": NumberDecimal("99990.00"),
"total": NumberDecimal("99990.00"),
"status": "pending",
"shipping": {
"country": "RU",
"city": "Москва",
"street": "Ленина, 5"
},
"created_at": ISODate("2026-10-15T10:00:00Z")
}
Что здесь:
items— массив, а не отдельная коллекция. Позиции всегда читаются с заказом.shipping— вложенный документ, snapshot адреса.NumberDecimal— точные цены.
Коллекция vs отдельная
Вложенная коллекция нужна, если:
- 1:мало (не растёт бесконечно).
- Всегда читается вместе.
- Атомарные обновления важны.
Отдельная коллекция, если:
- Растёт неограниченно (например, сообщения в чате).
- Нужен поиск по ней независимо.
- Обновляется отдельно.
- Используется в других контекстах.
Правило: документ — aggregate (целостная сущность). Разбивай, если части живут отдельной жизнью.
Массовые операции
Bulk write:
db.users.bulkWrite([
{ insertOne: { document: { email: 'a@b.com', name: 'Алиса' } } },
{ updateOne: {
filter: { email: 'c@d.com' },
update: { $set: { name: 'Боб' } },
} },
{ deleteOne: { filter: { email: 'x@y.com' } } },
])
Что даёт: одна операция для нескольких изменений. Быстрее, чем по одной.
Порядок и ordered
db.users.insertMany([...], { ordered: true }) // default
// Остановится на первой ошибке
db.users.insertMany([...], { ordered: false })
// Продолжит, соберёт ошибки
Правило: для миграций и bulk — ordered: false.
Частые ошибки
1. Всё в один документ:
Растёт бесконечно → 16 MB лимит
Решение: отдельная коллекция.
2. Нет _id в проекции:
db.users.find({}, { email: 1 })
// _id всё равно вернётся
Решение: явно { _id: 0 }.
3. deleteMany({}) вместо drop():
Медленнее, индексы остаются
Решение: db.collection.drop().
4. Обновление через полную замену:
db.users.replaceOne({ _id }, { name: 'X' })
// стёрлись все остальные поля
Решение: $set.
5. Дубликаты через updateMany без upsert:
Несколько одинаковых документов
Решение: unique-индекс.
6. Хранение файлов в документе:
Быстро упрёшься в 16 MB
Решение: GridFS или S3.
7. find без индекса:
Collection scan
Решение: explain() + индекс.
Практика
1. Создайте коллекцию products:
db.products.insertMany([
{
sku: 'SKU001',
name: 'iPhone 15',
price: NumberDecimal('99990.00'),
tags: ['phone', 'apple'],
attributes: { color: 'black', storage: 256 },
},
{
sku: 'SKU002',
name: 'Galaxy S24',
price: NumberDecimal('79990.00'),
tags: ['phone', 'samsung'],
attributes: { color: 'white', storage: 128 },
},
])
2. Найдите товары с тегом apple:
db.products.find({ tags: 'apple' })
3. Обновите цену:
db.products.updateOne(
{ sku: 'SKU001' },
{ $set: { price: NumberDecimal('89990.00') }, $currentDate: { updated_at: true } }
)
4. Добавьте тег:
db.products.updateOne({ sku: 'SKU001' }, { $addToSet: { tags: 'sale' } })
5. Найдите по вложенному:
db.products.find({ 'attributes.color': 'black' })
6. Создайте индекс на tags:
db.products.createIndex({ tags: 1 })
Итог
- Документ — BSON-объект, до 16 MB.
- Коллекция — без схемы, но с валидацией.
_id— обязательное, неизменяемое.$set,$inc,$push,$pull,$addToSet— операторы.- Вложенность — если всегда читается вместе.
- Отдельная коллекция — если растёт или поиск отдельно.
ordered: false— для bulk.- GridFS — для файлов > 16 MB.
Дальше — CRUD в MongoDB.