Раздел 36 · MongoDB с нуля

Встраивание против ссылок

junior~20 мин

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

В прошлом уроке закончился кластер про CRUD: ты умеешь вставлять, искать, менять и удалять документы. Но мы ни разу не спросили, почему документы учебной базы магазина устроены так. Почему автор лежит прямо внутри книги, а покупатель в заказе записан одним номером? С этого урока начинается моделирование данных, и первый его вопрос главный: вкладывать или ссылаться.

Чемодан и камера хранения

Ты едешь на неделю в другой город. Самое нужное кладёшь в чемодан: зарядку, документы, одежду. Это всегда при тебе, достаёшь одним движением. Лыжи с собой не потащишь: сдашь их в камеру хранения и возьмёшь квитанцию. Лыжи пригодятся раз в поездке, и за ними придётся сходить отдельно.

У документа в MongoDB те же два варианта:

  • Встроить. Положить связанные данные прямо внутрь документа, как вещи в чемодан. Это встраивание.
  • Сослаться. Хранить в документе только номер другого документа, как квитанцию. Это ссылка.

В PostgreSQL выбора нет: связанные данные живут в разных таблицах, а собирает их внешний ключ и JOIN. Реляционная теория требует нормализации: каждый факт хранится один раз. MongoDB разрешает обе стратегии и оставляет выбор тебе.

Как это устроено в учебной базе

Посмотрим на книгу «Грокаем алгоритмы»:

db.books.findOne({ _id: 7 }, { title: 1, author: 1, reviews: 1 })
{
  _id: 7,
  title: 'Грокаем алгоритмы',
  author: { _id: 4, name: 'Бхаргава', country: 'США' },
  reviews: [
    { user: 'Павел', rating: 5, text: 'Понятно даже новичку' },
    { user: 'Светлана', rating: 3, text: 'Слишком просто' }
  ]
}

Здесь встроено два куска:

  • Отзывы лежат массивом внутри книги. Отдельно от книги отзыв никому не нужен, а на странице книги его показывают всегда.
  • Сводка об авторе: имя и страна. Полная карточка автора живёт в коллекции authors, а в книге лежит копия того, что нужно для витрины. Об этом приёме подробно в уроке про денормализацию.

Одним запросом мы получили всё, что нужно для страницы книги. В PostgreSQL здесь были бы три таблицы и два соединения.

А вот заказы устроены иначе. Покупатель в заказе записан одним полем customer_id, это ссылка. Чтобы показать Ольге историю заказов, нужно два шага:

const olga = db.customers.findOne({ name: 'Ольга' })
db.orders.find({ customer_id: olga._id }, { status: 1, created_at: 1 }).sort({ created_at: 1, _id: 1 })
[
  {
    _id: 1,
    status: 'delivered',
    created_at: ISODate('2026-09-01T00:00:00.000Z')
  },
  {
    _id: 2,
    status: 'delivered',
    created_at: ISODate('2026-09-14T00:00:00.000Z')
  }
]

Первый запрос находит покупателя, второй ищет его заказы по ссылке. Так это выглядит и в коде приложения: два обращения к базе. Собрать то же одним запросом умеет этап $lookup, его разберём в уроке про конвейер агрегации.

Почему заказы не встроены в покупателя? Их число растёт всю жизнь клиента, а читать их нужно отдельно: «все заказы за сентябрь» не должно перебирать каждого покупателя.

урок из курса

Урок из курса «MongoDB с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.