В реляционке вы моделируете данные под нормализацию: сущности → таблицы → внешние ключи, чтобы каждый факт лежал ровно в одном месте. В NoSQL логика обратная — вы моделируете под паттерн доступа: сначала смотрите, какие запросы будут читать данные, и складываете данные так, чтобы каждый запрос обслуживался одним чтением. Главный инструмент документных баз — агрегат: связанные данные (заказ вместе со строками) лежат в одном документе. Отсюда два решения на каждой связи: embedding (вложить) или referencing (сослаться). Цена документной модели — денормализация: данные дублируются, и при обновлении их приходится синхронизировать вручную, часто с eventual consistency. Правило: «query-first design» — сначала запросы, потом модель, а не наоборот.
Однажды я спроектировал каталог товаров в MongoDB ровно так, как привык в PostgreSQL: товары в одной коллекции, категории в другой, бренды в третьей, везде ссылки по id. Красиво, нормализовано, ни байта дублирования. А потом пришёл главный экран приложения — карточка товара, где надо показать название, бренд, категорию и характеристики разом. На каждый показ карточки приложение делало четыре запроса в базу и склеивало их в коде. На реляционке я бы написал один JOIN и забыл. Но в MongoDB JOIN-ов в привычном смысле нет, и я получил медленный экран на ровном месте. Я моделировал под нормализацию там, где надо было моделировать под запрос.
Это и есть ключевая разница, которую недоговаривают, когда учат моделированию на реляционных примерах. В записи про три уровня модели данных моделирование честно доходит до реляционной физики — таблицы, типы, индексы. Но в NoSQL физический уровень устроен иначе, и сама логика проектирования переворачивается: не «какие у меня сущности», а «какие у меня запросы».
Нормализация против паттерна доступа
В реляционной базе вы исходите из структуры данных. Есть сущности, у них связи, вы раскладываете их по таблицам так, чтобы не было дублирования: имя клиента — в одном месте, его заказы ссылаются на него по ключу. Это нормализация, и она подробно разобрана в записи про базы данных для аналитика. Преимущество: один факт — одно место, обновил — и он сразу верен везде. А собрать данные обратно из разных таблиц вам помогает JOIN, и реляционная база делает это хорошо.
В NoSQL (возьмём документную базу как главный пример) JOIN-ов либо нет, либо они дорогие и неудобные. Значит, склеивать данные из разных мест на каждый запрос — больно. Поэтому проектирование начинают с другого конца: выписывают паттерны доступа — список запросов, которые приложение реально будет делать. «Показать карточку товара», «показать заказ со всеми строками», «список заказов клиента». А потом складывают данные так, чтобы каждый такой запрос обслуживался по возможности одним чтением одного документа. Это и называют query-first design: сначала запросы, потом модель.
Разница в одну фразу
В реляционке вы моделируете данные так, чтобы их было удобно хранить без дублирования, и платите за чтение JOIN-ами. В NoSQL вы моделируете данные так, чтобы их было удобно читать под конкретные запросы, и платите за это дублированием при записи. Это прямой размен: цена удобного чтения — неудобная запись, и наоборот.
Агрегат: заказ как один документ
Центральное понятие документной модели — агрегат (aggregate). Это группа данных, которые живут и читаются вместе как единое целое. Классический пример — заказ: у него есть шапка (номер, дата, клиент, сумма) и строки (товар, количество, цена за штуку). В реальной жизни заказ почти всегда нужен целиком: вы не показываете строки заказа в отрыве от самого заказа. Значит, заказ со строками — это естественный агрегат, и в документной базе он логично ложится в один документ.
Сравним одну и ту же сущность «Заказ» в двух мирах. Реляционно это две таблицы со связью один-ко-многим и внешним ключом:
-- PostgreSQL: нормализованно, две таблицы
order(
id BIGSERIAL PK,
customer BIGINT,
created TIMESTAMP,
total NUMERIC(10,2)
)
order_item(
id BIGSERIAL PK,
order_id BIGINT FK -> order(id),
product VARCHAR,
qty INT,
price NUMERIC(10,2)
)
Чтобы собрать заказ целиком, вы делаете JOIN таблиц order и order_item по order_id. А в MongoDB тот же заказ — это один документ, где строки вложены прямо внутрь:
// MongoDB: один документ-агрегат, items вложены (embedding)
{
_id: 5012,
customer: 318,
created: "2026-05-24T10:30:00Z",
total: 4500.00,
items: [
{ product: "Клавиатура", qty: 1, price: 3000.00 },
{ product: "Мышь", qty: 1, price: 1500.00 }
]
}
Trade-off виден сразу. Документ читается одним запросом по _id — весь заказ приезжает целиком, без склейки. Это быстро и удобно для главного паттерна доступа «открыть заказ». Но если вам понадобится отчёт «сколько всего продано клавиатур по всем заказам», в реляционке это простой GROUP BY по order_item, а в документной базе придётся обходить массивы items внутри тысяч документов — гораздо менее естественная операция. Документ хорош под чтение агрегата целиком и плох под аналитику поперёк агрегатов.
Embedding или referencing
На каждой связи в документной модели вы принимаете решение: вложить данные внутрь документа (embedding) или хранить отдельно и ссылаться по id (referencing). Это и есть основной рычаг проектирования.
flowchart TD
Q{Данные читаются
вместе с родителем?} --> |Да, почти всегда| E[Embedding:
вложить в документ]
Q --> |Нет, нужны и отдельно| R[Referencing:
хранить отдельно, ссылка по id]
E --> E2{Их много
и они растут?}
E2 --> |Нет, ограниченный набор| EOK[Вкладывать ок]
E2 --> |Да, безграничный рост| R
Схема выше — простое правило выбора. Сначала спрашиваете: эти данные почти всегда читаются вместе с родителем? Если да (строки заказа всегда нужны вместе с заказом) — вкладываете внутрь, это embedding. Если данные нужны и сами по себе, отдельно от родителя (клиент существует независимо от конкретного заказа) — храните отдельно и ссылаетесь по id, это referencing. Но у embedding есть второй вопрос: этих вложенных данных ограниченное число или они растут безгранично? Строк в заказе — единицы или десятки, их можно смело вкладывать. А вот комментарии к посту, которых могут быть миллионы, вкладывать нельзя: документ имеет предел размера (в MongoDB — 16 МБ) и распухнет. Безгранично растущие коллекции выносят в отдельные документы со ссылкой.
Грубое правило: embedding — для данных, которые принадлежат родителю и читаются с ним вместе (строки заказа, адрес в профиле). Referencing — для самостоятельных сущностей и для всего, что растёт без предела (клиенты, товары, лента комментариев).
Денормализация и её цена
Embedding и дублирование данных под запросы — это денормализация: один и тот же факт намеренно лежит в нескольких местах. Допустим, в каждом заказе вы храните не только id клиента, но и его имя, чтобы показывать список заказов без обращения к коллекции клиентов. Чтение ускорилось. Но появилась цена.
Дублирование означает ручную консистентность
Если имя клиента продублировано в тысяче его заказов, то при смене имени вам надо обновить эту тысячу документов. База этого за вас не сделает — в реляционке нормализация гарантировала, что имя лежит в одном месте и менять его надо однократно. В денормализованной модели за согласованность отвечаете вы, в коде приложения. Забыли обновить часть копий — данные разъехались, и в одних заказах клиент «Иван Петров», а в других уже «Иван Сидоров». Это не теоретический риск, это самый частый баг денормализованных моделей.
Часто синхронизацию копий делают не мгновенно, а фоново — через события: «имя клиента изменилось» летит в очередь, и подписчики обновляют свои копии. Это смыкается с событийной архитектурой и приносит eventual consistency: какое-то время копии расходятся, и лишь спустя секунды-минуты сходятся. Для имени клиента в списке заказов это терпимо. Для остатка на складе или баланса счёта — нет, и это сигнал, что такие данные просят реляционную базу с транзакциями, а не денормализованный документ.
Ключ-значение и wide-column кратко
Документная модель — не единственная. Два других семейства NoSQL моделируют под доступ ещё жёстче. Key-value (Redis) — это словарь: вы кладёте значение по ключу и достаёте по ключу, и весь паттерн доступа — «дай по ключу». Никаких запросов по содержимому, никаких связей; зато сверхбыстро. Идеально под сессии, счётчики, кеш. Wide-column (Cassandra) — таблицы, но проектируются они вокруг конкретного запроса: ключ строки и порядок столбцов выбирают так, чтобы нужный запрос читался одной операцией. В Cassandra нормальная практика — завести отдельную таблицу под каждый паттерн доступа, продублировав данные. Тут query-first доведён до предела: модель — это буквально слепок ваших запросов. Что когда брать из семейств NoSQL — разобрано в записи про SQL или NoSQL.
Откуда это взялось
Реляционная модель и нормализация царили с 1970-х, и моделировать «под запросы» считалось ересью. Перелом устроили веб-гиганты, упёршиеся в масштаб. В 2006 году Google опубликовал статью про Bigtable — wide-column-хранилище под веб-масштаб, а в 2007-м Amazon описал Dynamo — key-value-систему, спроектированную ради доступности корзины любой ценой. Эти две работы дали словарь всему движению. MongoDB вышла в 2009 году и популяризовала именно документную модель с агрегатами. Тогда же, около 2009-го, закрепился и сам термин «NoSQL» — изначально как хэштег встречи разработчиков, и расшифровывали его не «нет SQL», а «Not Only SQL». Суть сдвига: от «нормализуй данные, а запросы соберут их обратно» к «смотри на запросы и складывай данные под них».
Как это спрашивают на собесе
«Чем моделирование в NoSQL отличается от реляционного?» — отвечайте через ось «нормализация против паттерна доступа»: в SQL моделируешь структуру и нормализуешь, в NoSQL моделируешь под запросы (query-first) и осознанно денормализуешь. «Embedding или referencing — как выбрать?» — проверяют, понимаете ли вы критерии: читается ли вместе с родителем и не растёт ли безгранично. Подвох-вопрос: «А в чём минус документной модели?» — правильный ответ называет дублирование, ручную консистентность и дорогие изменения формы данных, а не только хвалит скорость. Если уверенно говорите про trade-off, а не про «MongoDB гибкая» — вы прошли.
Честно про минусы
NoSQL-моделирование — не «лучше», а «про другое», и у него есть прямая цена. Первое — дублирование: денормализация раздувает объём и плодит копии. Второе — ручная консистентность: за синхронизацию копий отвечает ваш код, база не подстрахует, и это богатый источник багов. Третье, и самое недооценённое, — дорогая смена формы данных. В реляционке вы продумываете схему один раз под все запросы. В NoSQL вы заточили документ под сегодняшние паттерны доступа — а завтра пришёл новый запрос, под который данные лежат неудобно. Перекроить миллионы документов под новую форму — это полноценная миграция с бэкфиллом, и она ничуть не дешевле реляционной: как такое делается без даунтайма — в записи про миграции данных и бэкфилл. Гибкость схемы NoSQL соблазнительна на старте и мстит на длинной дистанции, когда паттерны доступа меняются.
Частые вопросы
Что такое агрегат в документной модели?
Агрегат (aggregate) — это группа связанных данных, которые читаются и живут вместе как одно целое, и поэтому хранятся в одном документе. Классический пример — заказ со своими строками: вы почти всегда показываете строки вместе с заказом, значит они образуют один агрегат и ложатся в один документ MongoDB. Граница агрегата определяется паттерном доступа: что читается вместе — то и группируется. Это противоположность реляционному подходу, где заказ и его строки лежат в разных нормализованных таблицах.
Когда вкладывать данные (embedding), а когда ссылаться (referencing)?
Вкладывайте (embedding), если данные почти всегда читаются вместе с родителем и их ограниченное число — строки заказа, адрес в профиле. Ссылайтесь (referencing), если данные нужны самостоятельно, отдельно от родителя (клиент, товар), или если они растут безгранично (лента комментариев, события) — иначе документ распухнет и упрётся в предел размера. Короткое правило: «принадлежит и читается вместе, и его немного» — вкладывать; «самостоятельно или растёт без предела» — ссылаться.
Почему денормализация — это риск, а не просто оптимизация?
Потому что один факт оказывается в нескольких местах, и при его изменении все копии надо обновить вручную в коде приложения — база этого не гарантирует, в отличие от нормализованной схемы, где факт лежит в одном месте. Часто синхронизацию делают фоново через события, и тогда копии какое-то время расходятся (eventual consistency). Для второстепенных данных вроде имени клиента в списке это терпимо, а для остатков и денег — нет, и это сигнал брать реляционную базу с транзакциями.