Проектирование схемы в MongoDB: вложение или ссылки
Практика3 минРедакция Innovialabs
Коротко
Вкладывайте данные, которые читаются вместе и ограничены по размеру, ссылайтесь на общие, большие или бесконечно растущие данные, а схему стройте вокруг самых частых запросов.
Правило в одном абзаце
В MongoDB схему проектируют вокруг того, как приложение читает и пишет данные, а не вокруг абстрактных сущностей. Вкладывайте данные внутрь документа, если они читаются вместе с родителем, принадлежат только ему и остаются небольшими. Ссылайтесь по _id, если данные общие для многих документов, меняются независимо, занимают много места или могут расти без ограничений.
Вложение и ссылки: сравнение
| Вопрос | Вложение | Ссылка |
|---|---|---|
| Читается вместе с родителем? | Почти всегда | Часто отдельно |
| Принадлежит одному родителю? | Да | Используется многими |
| Размер ограничен? | Небольшой, есть предел | Растёт со временем |
| Меняется независимо? | Редко | Часто |
| Нужен снимок на момент события? | Да, вкладываем копию | — |
Помните о жёстком лимите размера документа — 16 МБ. Задолго до него большие документы замедляют каждое чтение и обновление.
Модель: пользователи, товары, заказы
Пользователи
Адресов и настроек уведомлений немного, и они всегда читаются вместе с профилем — вкладываем.
{
_id: ObjectId("..."),
email: "user@example.com",
name: "Азиза",
addresses: [
{ label: "home", city: "Tashkent", street: "..." }
],
settings: { language: "ru", notifications: true }
}
Заказы в пользователя не вкладываем: их число растёт бесконечно.
Товары
Товар используется многими заказами и корзинами, поэтому живёт в своей коллекции. Характеристики, которые зависят от категории, удобно хранить во вложенном объекте.
{
_id: ObjectId("..."),
sku: "TSHIRT-001",
title: "Футболка",
price: 120000,
categoryId: ObjectId("..."),
attributes: { size: ["S", "M", "L"], color: "black" },
stock: 42
}
Заказы
Заказ ссылается на пользователя, но вкладывает снимок позиций: название и цену на момент покупки. Если завтра цена товара изменится, старый заказ меняться не должен.
{
_id: ObjectId("..."),
userId: ObjectId("..."),
status: "paid",
createdAt: ISODate("..."),
items: [
{ productId: ObjectId("..."), title: "Футболка", price: 120000, qty: 2 }
],
total: 240000,
shipping: { city: "Tashkent", street: "..." }
}
Это паттерн extended reference: храним _id для связи и копируем несколько полей, нужных для отображения.
Избегайте бесконечных массивов
Массив, который растёт без предела, — все комментарии к посту, все события устройства, все заказы пользователя, — самая частая ошибка в MongoDB. Он приводит к огромным документам, медленным обновлениям и риску упереться в лимит размера.
Что делать вместо этого:
- Отдельная коллекция со ссылкой на родителя:
commentsс полемpostIdи индексом по нему. - Паттерн subset: вкладываем только несколько последних элементов (например, свежие отзывы), а полный список храним отдельно.
- Паттерн bucket для временных рядов: группируем события в один документ на устройство за час или день.
Индексы под сценарии доступа
Сначала выпишите основные запросы, затем создайте под них индексы.
db.orders.createIndex({ userId: 1, createdAt: -1 }) // история заказов пользователя
db.orders.createIndex({ status: 1, createdAt: -1 }) // список в админке по статусу
db.products.createIndex({ sku: 1 }, { unique: true })
db.users.createIndex({ email: 1 }, { unique: true })
Практические правила:
- В составных индексах соблюдайте порядок ESR: поля точного совпадения (Equality), затем сортировки (Sort), затем диапазона (Range).
- Индексы по массивам (multikey) работают, но большие массивы делают их тяжёлыми.
- Проверяйте запросы через
explain("executionStats"): нуженIXSCAN, а неCOLLSCAN. - Каждый индекс замедляет запись и занимает память — удаляйте неиспользуемые.
Частые ошибки
- Перенос реляционной схемы один в один и эмуляция JOIN через множество
$lookup. - Вложение общих данных (например, всего товара) повсюду, после чего приходится обновлять тысячи копий.
- Хранение денег в числах с плавающей точкой вместо целых чисел в минимальных единицах или
Decimal128. - Отказ от валидации: MongoDB поддерживает валидаторы
$jsonSchemaдля коллекций.