Перейти к содержанию
Innovialabs

Проектирование схемы в MongoDB: вложение или ссылки

Практика3 мин

Коротко

Вкладывайте данные, которые читаются вместе и ограничены по размеру, ссылайтесь на общие, большие или бесконечно растущие данные, а схему стройте вокруг самых частых запросов.

Правило в одном абзаце

В 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 для коллекций.

Частые вопросы

Да, через стадию агрегации $lookup. Для редких запросов это нормально, но если большинству чтений нужны соединения, данные, скорее всего, реляционные и SQL-база подойдёт лучше.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.