Модель документа
Документная БД хранит данные как самодостаточные документы (в MongoDB — BSON, бинарный JSON с типами и датами). Документы группируют в коллекции, схема гибкая: в одной коллекции документы могут отличаться полями.
Гибкость не значит «без схемы»: схема просто живёт в коде и в соглашениях. Без дисциплины коллекция превращается в свалку разнородных документов, которую тяжело поддерживать.
Embedding против referencing
Главное решение моделирования — вкладывать связанные данные в документ (embedding) или хранить ссылки на другие документы (referencing). Embedding даёт чтение агрегата одним запросом и атомарность изменений, но раздувает документ и дублирует данные.
Эмпирика: вкладывайте данные, которые читаются вместе с родителем и принадлежат ему («содержит»), и ссылайтесь на сущности с собственной жизнью, разделяемые многими, или растущие без предела.
Антипаттерн — unbounded array: вкладывать в документ коллекцию, которая растёт неограниченно (например, все комментарии к посту). Документ упрётся в лимит размера и станет дорогим для обновления.
// embedding: адрес читается всегда вместе с пользователем
{ _id: 1, name: "Ann", address: { city: "Riga", zip: "LV-1001" } }
// referencing: заказы живут своей жизнью и растут — ссылка
{ _id: 1, name: "Ann" }
{ _id: 99, userId: 1, total: 4200 }
Индексы
Как и в SQL, без индексов запросы делают полный скан коллекции. Есть single-field, compound (с тем же правилом левого префикса, что у B-tree), multikey (по элементам массива), text и geo индексы.
Индексы ускоряют чтение и фильтрацию, но замедляют запись и едят память — баланс тот же, что в реляционных БД. План запроса смотрят через explain().
Aggregation pipeline
Сложные выборки и аналитику делают через aggregation pipeline — конвейер стадий ($match, $group, $sort, $lookup и др.), где документы текут от стадии к стадии.
$match ставят как можно раньше (аналог раннего WHERE), чтобы сократить поток и задействовать индексы. $lookup умеет джойнить коллекции, но это не дешёвая операция и не замена правильной модели.
db.orders.aggregate([
{ $match: { status: "PAID" } }, // раньше = дешевле
{ $group: { _id: "$userId", total: { $sum: "$amount" } } },
{ $sort: { total: -1 } }
])
Атомарность и транзакции
Операция над одним документом атомарна — это базовая гарантия MongoDB, и грамотный embedding часто позволяет уложить инвариант в один документ и обойтись без многодокументных транзакций.
Многодокументные транзакции существуют, но они дороже и медленнее; полагаться на них как в реляционной БД — признак того, что модель документов спроектирована не под задачу.
Шардирование
При горизонтальном масштабировании коллекцию шардируют по shard key. Хороший ключ даёт равномерное распределение и направляет частые запросы в одну партицию; плохой создаёт горячие чанки и заставляет запрос обходить все шарды (scatter-gather).
Монотонно растущий ключ (например, время) — классическая ошибка: вся свежая запись льётся в один шард.
Как отвечать на собеседовании
Сильный ответ: объясните выбор embedding/referencing через «читается вместе/принадлежит» и предел роста, упомяните атомарность документа как способ избежать транзакций, и назовите риск unbounded array и плохого shard key.
Начните с честной оговорки про «schema-less»: схема есть всегда, она просто переезжает из базы в код и соглашения. Гибкость означает, что базой она больше не проверяется, — а значит, дисциплина нужна большая, а не меньшая. Такое начало сразу снимает подозрение, что кандидат выбирает MongoDB «чтобы не думать о схеме».
Ключевой вопрос моделирования формулируйте как выбор с критерием: вкладываем то, что читается вместе с родителем и принадлежит ему, ссылаемся на сущности с собственной жизнью, разделяемые многими или растущие без предела. И назовите антипаттерн unbounded array (все комментарии внутри поста) — документ упирается в лимит размера и дорожает на каждом обновлении.
Про транзакции дайте неочевидную связку: операция над ОДНИМ документом атомарна, поэтому удачный embedding часто позволяет уложить инвариант в один документ и вообще не понадобиться многодокументным транзакциям. Они существуют, но дороже, и постоянная нужда в них — сигнал, что модель спроектирована не под задачу.
Отметьте, что базовые принципы не отменяются: индексы работают по тому же правилу левого префикса, план смотрят через explain(), а $match в pipeline ставят как можно раньше — это тот же ранний WHERE.
Завершите шардированием: хороший shard key даёт равномерное распределение и направляет частые запросы в одну партицию, плохой порождает scatter-gather по всем шардам. И назовите классическую ошибку — монотонно растущий ключ вроде времени, из-за которого вся свежая запись льётся в один шард.