Document-моделирование: embedding и referencing
Все предыдущие модули курса жили в реляционном мире: таблицы, строки, нормализация, JOIN. Но реляционная модель — не единственная. Существует целое семейство NoSQL-хранилищ, и они требуют другого мышления о данных. Этот модуль — про моделирование для NoSQL, и начнём мы с самого популярного его вида: документных баз (document databases), флагман которых — MongoDB.
Сразу развеем опасное заблуждение, с которым новички часто приходят в NoSQL: «NoSQL не требует моделирования, просто кидаешь JSON». Это неправда — и дорогая. NoSQL требует моделирования не меньше, а больше дисциплины, чем реляционные базы. Просто другой дисциплины. И первое ключевое решение document-моделирования — выбор между embedding и referencing.
Что такое документ
В реляционной базе данные — это строки в таблицах с фиксированной схемой. В документной базе единица данных — документ: структура в формате JSON (внутри MongoDB — бинарный вариант, BSON). Документ может содержать вложенные объекты и массивы, и у документов одной коллекции не обязательно одинаковый набор полей.
Вот как выглядит документ заказа:
{
"_id": "ORD-5501",
"order_date": "2026-05-20",
"status": "paid",
"amount": 4500,
"items": [
{ "product": "Клавиатура", "qty": 1, "price": 3000 },
{ "product": "Мышь", "qty": 1, "price": 1500 }
]
}
Обратите внимание: позиции заказа (items) лежат внутри документа заказа, массивом. В реляционной модели это были бы отдельная таблица order_items и JOIN. Документная модель позволяет хранить связанные данные вместе, вложенными. Позволяет — но не обязывает. И вот тут начинается главный выбор.
Два способа связать данные
Когда одна сущность связана с другой (заказ и его позиции, клиент и его адреса, статья и её комментарии), document-модель предлагает два пути.
Embedding (вложение) — поместить связанные данные внутрь родительского документа. Позиции заказа лежат массивом прямо в документе заказа, как в примере выше. Одна сущность физически содержит другую.
Referencing (ссылка) — хранить связанные данные в отдельной коллекции, а в родительском документе держать только ссылку (_id связанного документа). Это похоже на foreign key из реляционного мира.
// Referencing: заказ ссылается на товары по _id
{
"_id": "ORD-5501",
"status": "paid",
"items": [
{ "product_id": "SKU-100", "qty": 1 },
{ "product_id": "SKU-200", "qty": 1 }
]
}
// Документы товаров живут в отдельной коллекции products:
{ "_id": "SKU-100", "name": "Клавиатура", "price": 3000, "stock": 42 }
{ "_id": "SKU-200", "name": "Мышь", "price": 1500, "stock": 17 }
Выбор между ними — не вкусовщина. Это центральное проектное решение document-моделирования, и от него зависит, будет база быстрой или медленной.
Почему embedding часто выигрывает: физика чтения
Чтобы выбирать осознанно, нужно понять физику. Главная операция, под которую оптимизируют документную базу, — достать документ по ключу.
При embedding связанные данные лежат внутри документа. Запрос «дай заказ ORD-5501 со всеми позициями» — это одно чтение одного документа. База нашла документ по _id и вернула его целиком, с вложенным массивом позиций. Один поход к диску.
При referencing данные разнесены. Тот же запрос — это: прочитать документ заказа, увидеть в нём ссылки на товары, затем сходить за каждым товаром в коллекцию products. Несколько отдельных чтений. У документных баз нет дешёвого серверного JOIN, как в SQL, — связывание ссылок обычно ложится на приложение или на отдельную операцию.
Отсюда — фундаментальный принцип document-моделирования: данные, которые читаются вместе, должны храниться вместе. Если приложение почти всегда показывает заказ сразу с позициями — позиции надо embed. Тогда типичный запрос становится одним быстрым чтением. Это прямая противоположность реляционной нормализации, где данные сознательно разносят по таблицам. В document-модели разнесение данных — это лишние чтения.
Когда нужен referencing
Если embedding так хорош для чтения — почему не вкладывать всё всегда? Потому что у embedding есть пределы, и в нескольких ситуациях referencing необходим.
Документ растёт без ограничений. Документная база ограничивает размер одного документа (в MongoDB — 16 МБ). Если вкладывать в документ массив, который растёт бесконечно (все события пользователя за годы, все комментарии вирусного поста), документ рано или поздно упрётся в лимит. Безграничные «многие» нужно referencing.
Данные меняются независимо. Если вложенные данные часто обновляются сами по себе, embedding мешает. Цена товара, вложенная копией в тысячи заказов, при изменении потребовала бы обновить тысячи документов. В отдельной коллекции products цена правится в одном месте. Часто и независимо изменяемое — referencing.
Связь many-to-many. Студенты и курсы: студент записан на много курсов, курс содержит много студентов. Вложить одно в другое нельзя без огромного дублирования. Many-to-many — referencing (обычно с массивом ссылок).
К данным нужен независимый доступ. Если товар надо запрашивать сам по себе — каталог, поиск, остатки, — а не только в контексте заказа, ему нужна своя коллекция. Сущность, к которой обращаются независимо, — referencing.
| Ситуация | Решение |
|---|---|
| Данные читаются вместе с родителем | Embedding |
| Связь «содержит», 1:N ограниченной мощности | Embedding |
| Безграничный рост вложенного массива | Referencing |
| Вложенное часто меняется независимо | Referencing |
| Связь many-to-many | Referencing |
| Нужен независимый доступ к сущности | Referencing |
Практическое правило для выбора. Задайте три вопроса: (1) Эти данные читаются вместе или по отдельности? (2) Сколько их — ограниченное число или растёт безгранично? (3) Меняются вместе с родителем или своей жизнью? Вместе + ограниченно + меняются с родителем -> embedding. Иначе -> referencing. Большинство реальных случаев решается этими тремя вопросами.
Денормализация как норма
Ещё одно отличие от реляционного мышления. В реляционной модели дублирование данных — это аномалия, с которой борется нормализация. В document-модели дублирование часто нормально и желательно.
Пример. В документе заказа удобно хранить имя клиента прямо в заказе — даже если у клиента есть отдельная коллекция. Да, имя продублировано. Но запрос «покажи заказ с именем покупателя» становится одним чтением, без обращения к коллекции клиентов.
{
"_id": "ORD-5501",
"customer_id": "CUST-001",
"customer_name": "Анна Петрова",
"status": "paid"
}
Это сознательная денормализация ради скорости чтения — та же идея, что OBT в аналитике из прошлого модуля. Цена та же: если Анна сменит имя, его придётся обновить во всех заказах (либо смириться, что в старых заказах останется имя «как было на момент заказа» — что часто даже правильно). Document-моделирование постоянно делает этот выбор: дублировать ради быстрого чтения или ссылаться ради лёгкого обновления.
Вот ключевая смена мышления всего модуля. Реляционная модель проектируется от структуры данных и связей — а запросы потом приспосабливаются к схеме. NoSQL, включая document, проектируется от запросов — сначала понимаем, как приложение будет читать данные, и под это строим структуру. В реляционном мире нормализация первична, доступ вторичен. В document-мире access pattern первичен, структура подстраивается под него. Эту идею «query-first» мы будем развивать весь модуль.
Попробуй сам
Возьмите блог с сущностями: пользователь, статья, комментарий, тег.
- Для пары «статья и её комментарии» решите: embedding или referencing? Пройдите три вопроса из правила выше. Учтите, что у вирусной статьи комментариев может быть очень много.
- Для пары «статья и её теги» решите то же самое. Теги — это many-to-many (один тег у многих статей).
- Спроектируйте документ статьи в JSON: что вложите, на что сошлётесь.
- Решите, стоит ли дублировать имя автора в документ статьи. Какой запрос это ускорит и какой ценой при смене имени автора?
В следующем уроке перейдём к key-value базам, где моделирование сводится к дизайну структуры ключа.