CRUD в MongoDB
Create
Один документ:
db.users.insertOne({
email: 'alice@example.com',
name: 'Алиса',
age: 30,
})
Много:
db.users.insertMany([
{ email: 'a@b.com', name: 'Алиса' },
{ email: 'c@d.com', name: 'Боб' },
], { ordered: false })
С возвратом:
const result = await users.insertOne({ ... })
result.insertedId // ObjectId
Read
Все:
db.users.find()
Один:
db.users.findOne({ email: 'a@b.com' })
Проекция:
db.users.find(
{ is_active: true },
{ email: 1, name: 1, _id: 0 }
)
Сортировка + лимит:
db.users.find().sort({ created_at: -1 }).limit(10)
Пагинация:
const cursor = db.users
.find()
.sort({ created_at: -1 })
.skip(20)
.limit(10)
⚠️ Большой skip медленный. Для больших коллекций — cursor-based:
// page 1
db.users.find().sort({ _id: 1 }).limit(10)
// page 2 — после last_id
const last = '...'
db.users.find({ _id: { $gt: ObjectId(last) } }).sort({ _id: 1 }).limit(10)
Update
Один:
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 }
)
Replace:
db.users.replaceOne(
{ _id: userId },
{ email: 'new@b.com', name: 'New' }
)
// ⚠️ Всё, что не указано — удаляется
Правило: $set для частичного обновления, replaceOne — только если точно нужна полная замена.
Delete
Один:
db.users.deleteOne({ _id: userId })
Много:
db.users.deleteMany({ is_inactive: true })
Всё:
db.users.deleteMany({})
⚠️ deleteMany({}) — удаляет всё, но индексы сохраняются.
Операторы запросов
Сравнение:
{ age: 25 }
{ age: { $eq: 25 } }
{ age: { $ne: 25 } }
{ age: { $gt: 18 } }
{ age: { $gte: 18 } }
{ age: { $lt: 65 } }
{ age: { $lte: 65 } }
{ age: { $in: [18, 25, 30] } }
{ age: { $nin: [1, 2, 3] } }
Логические:
{ $and: [{ age: { $gte: 18 } }, { country: 'RU' }] }
{ $or: [{ status: 'paid' }, { status: 'shipped' }] }
{ $nor: [{ a: 1 }, { b: 2 }] }
{ age: { $not: { $gt: 18 } } }
Существование и тип:
{ bio: { $exists: true } }
{ bio: { $exists: false } }
{ age: { $type: 'int' } }
{ age: { $type: ['int', 'double'] } }
Regex:
{ email: { $regex: '^a', $options: 'i' } }
// $options: i — case-insensitive, m — multiline, x — extended
// Или через литерал
{ email: /^a/i }
Массивы:
{ tags: 'php' } // содержит php
{ tags: ['php', 'js'] } // точно этот массив
{ tags: { $all: ['php', 'js'] } } // содержит все
{ tags: { $size: 3 } } // длина 3
{ tags: { $elemMatch: { $gt: 5, $lt: 10 } } } // хотя бы один элемент подходит
Вложенные:
{ 'address.city': 'Москва' } // точечная нотация
{ address: { city: 'Москва', street: 'X' } } // точное совпадение документа (порядок важен)
Правило: для вложенных — точечная нотация. Точное совпадение документа — редко.
Логические операторы
$and:
{ $and: [{ age: { $gte: 18 } }, { age: { $lte: 65 } }] }
$or:
{ $or: [{ status: 'paid' }, { status: 'shipped' }] }
Комбинация:
{
$and: [
{ $or: [{ status: 'paid' }, { status: 'shipped' }] },
{ created_at: { $gte: ISODate('2026-01-01') } },
],
}
Что важно: $and можно опустить при нескольких полях — они и так AND:
{ age: { $gte: 18 }, country: 'RU' }
// = { $and: [{ age: { $gte: 18 } }, { country: 'RU' }] }
⚠️ Если два условия на одно поле — $and обязателен:
// ❌ Ошибка — второй перезапишет первый
{ age: { $gte: 18 }, age: { $lte: 65 } }
// ✅
{ $and: [{ age: { $gte: 18 } }, { age: { $lte: 65 } }] }
// Или короче
{ age: { $gte: 18, $lte: 65 } }
Проекция
Включение:
{ email: 1, name: 1 }
// _id тоже включится
Исключение:
{ password: 0, metadata: 0 }
⚠️ Нельзя смешивать включение и исключение (кроме _id).
Убрать _id:
{ email: 1, _id: 0 }
Из массива:
{ 'items.name': 1 }
// вернёт items как массив объектов только с name
Slice:
{ comments: { $slice: 5 } } // первые 5
{ comments: { $slice: -5 } } // последние 5
{ comments: { $slice: [10, 5] } } // с 10-го, 5 штук
$elemMatch в проекции:
{ items: { $elemMatch: { price: { $gt: 100 } } } }
// вернёт только первый подходящий элемент
Сортировка
.sort({ created_at: -1 })
.sort({ age: 1, name: -1 }) // два поля
По вложенному:
.sort({ 'address.city': 1 })
По массиву:
.sort({ tags: 1 }) // по минимальному элементу
Правило: без sort порядок не определён.
Лимиты и курсоры
.find().limit(10)
.find().skip(20).limit(10)
.find().batchSize(100) // размер батча по сети
Cursor — итератор:
const cursor = db.users.find({ is_active: true })
while (await cursor.hasNext()) {
const doc = await cursor.next()
// ...
}
Что даёт: не грузит всё в память сразу.
Count
db.users.countDocuments({ is_active: true })
db.users.estimatedDocumentCount() // быстрее, но приблизительно
Разница:
countDocuments— точно, медленнее.estimatedDocumentCount— из метаданных, быстро.
Distinct
db.users.distinct('country')
// [ 'RU', 'US', 'DE' ]
db.users.distinct('tags')
// все теги без дублей
Примеры запросов
Пользователи из России старше 18:
db.users.find({ country: 'RU', age: { $gte: 18 } })
Заказы за последние 7 дней, только оплаченные:
db.orders.find({
status: 'paid',
created_at: { $gte: new Date(Date.now() - 7 * 86400 * 1000) },
}).sort({ created_at: -1 })
Товары с тегом sale и ценой < 1000:
db.products.find({
tags: 'sale',
price: { $lt: NumberDecimal('1000') },
})
Пользователи без bio:
db.users.find({ bio: { $exists: false } })
// или
db.users.find({ bio: null })
⚠️ { bio: null } находит и null, и отсутствующие. $exists: false — только отсутствующие.
Производительность
explain():
db.users.find({ email: 'a@b.com' }).explain('executionStats')
Что смотреть:
executionStats.executionTimeMillis— время.executionStats.totalDocsExamined— сколько прочитано.executionStats.nReturned— сколько вернулось.winningPlan.stage—COLLSCAN(плохо) илиIXSCAN(хорошо).
Правило: totalDocsExamined / nReturned должно быть близко к 1.
Индекс:
db.users.createIndex({ email: 1 })
db.users.createIndex({ country: 1, age: -1 })
db.users.createIndex({ tags: 1 })
db.users.createIndex({ 'address.city': 1 })
db.users.createIndex({ email: 1 }, { unique: true })
Просмотр:
db.users.getIndexes()
Транзакции
С 4.0 (replica set):
const session = db.getMongo().startSession()
session.startTransaction()
try {
const users = session.getDatabase('myapp').users
const accounts = session.getDatabase('myapp').accounts
await users.updateOne(
{ _id: fromId },
{ $inc: { balance: -100 } },
{ session }
)
await accounts.updateOne(
{ _id: toId },
{ $inc: { balance: 100 } },
{ session }
)
await session.commitTransaction()
} catch (e) {
await session.abortTransaction()
throw e
} finally {
await session.endSession()
}
⚠️ Транзакции в MongoDB дорогие. Не использовать в hot path.
Правило: в 99% случаев структура документа решает проблему атомарности без транзакций.
Bulk write
db.users.bulkWrite([
{ insertOne: { document: { email: 'a@b.com' } } },
{ updateOne: { filter: { email: 'c@d.com' }, update: { $set: { name: 'X' } } } },
{ deleteOne: { filter: { email: 'x@y.com' } } },
])
Что даёт: одна операция, меньше round-trips.
Частые ошибки
1. find() без индекса:
COLLSCAN по миллионам
Решение: explain() + индекс.
2. $and не нужен:
{ $and: [{ a: 1 }, { b: 2 }] } // = { a: 1, b: 2 }
Решение: упростить.
3. Два условия на одно поле без $and:
{ age: { $gte: 18 }, age: { $lte: 65 } } // ошибка
Решение: { age: { $gte: 18, $lte: 65 } }.
4. skip больших значений:
skip(1000000) — медленно
Решение: cursor-based pagination.
5. replaceOne вместо $set:
Стирает поля
Решение: $set.
6. deleteMany({}) вместо drop():
Медленнее, индексы остаются
Решение: drop().
7. Транзакции для простых операций:
Дорого
Решение: документная структура решает.
8. countDocuments на миллионах:
Медленно
Решение: estimatedDocumentCount или кэш.
Практика
1. Создайте коллекцию, вставьте 5 документов.
2. Найдите по вложенному полю.
3. Обновите через $set, $inc, $push.
4. Удалите по условию.
5. Сделайте пагинацию через skip/limit.
6. Сделайте cursor-based пагинацию.
7. explain() до и после индекса.
8. Upsert: обновите существующего и создайте нового.
Итог
- CRUD:
insertOne,find,updateOne,deleteOne. - Операторы:
$set,$inc,$push,$pull. - Запросы:
$gt,$in,$exists,$regex,$elemMatch. - Проекция: включение или исключение.
- Пагинация: cursor-based для больших.
explain()— анализ.- Индекс — обязателен для частых запросов.
- Транзакции — дорого, только для критичного.
- Bulk write — для массовых.
Дальше — запросы и агрегация.