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

Денормализация и дублирование

junior~20 мин

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

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

Визитка в записной книжке

Друг дал тебе визитку, и ты переписал его телефон в три места: в телефон, в рабочий ежедневник и на листок у холодильника. Звонить стало удобно, номер всегда под рукой. Но друг сменил номер. Теперь его нужно исправить в трёх местах, и если забудешь про листок, однажды позвонишь не туда.

Это и есть денормализация: одни и те же данные хранятся в нескольких местах, чтобы быстрее читать. Расплата приходит при записи.

В реляционной теории денормализация это исключение, на которое идут осознанно и редко. Мы обсуждали это в уроке про BCNF и денормализацию. В MongoDB это обычный рабочий инструмент: соединений в привычном виде нет, поэтому данные, которые нужны на странице, кладут в документ этой страницы.

Копия сводки: extended reference

В учебной базе книга хранит не только номер автора, но и его имя и страну:

db.books.findOne({ _id: 3 }, { title: 1, author: 1 })
{
  _id: 3,
  title: 'Пикник на обочине',
  author: { _id: 2, name: 'Стругацкие', country: 'Россия' }
}

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

Теперь друг меняет номер. Магазин решил писать авторов полностью: «Аркадий и Борис Стругацкие». Сначала правим оригинал:

db.authors.updateOne({ _id: 2 }, { $set: { name: 'Аркадий и Борис Стругацкие' } })
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}

Потом каждую копию. Копии находим по ссылке author._id:

db.books.updateMany(
  { 'author._id': 2 },
  { $set: { 'author.name': 'Аркадий и Борис Стругацкие' } }
)
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 2,
  modifiedCount: 2,
  upsertedCount: 0
}

Две книги, две копии. Проверим:

db.books.find({ 'author._id': 2 }, { title: 1, 'author.name': 1 }).sort({ _id: 1 })
[
  {
    _id: 3,
    title: 'Пикник на обочине',
    author: { name: 'Аркадий и Борис Стругацкие' }
  },
  {
    _id: 4,
    title: 'Трудно быть богом',
    author: { name: 'Аркадий и Борис Стругацкие' }
  }
]

Вот и цена копии:

  • Изменение стало двумя командами. Если приложение упадёт между ними, автор будет называться по-новому, а книги по-старому. Такое расхождение называют рассинхронизацией. Связать две команды в одно целое помогают многодокументные транзакции, им посвящён отдельный урок ближе к концу раздела.
  • Запись стала дороже. У автора с сотней книг одна правка имени превращается в сотню обновлений.

Отсюда правило: копируй поля, которые читают часто, а меняют редко. Имя автора и страна меняются раз в жизни, поэтому копировать их выгодно. А цену или остаток на складе копировать в чужие документы нельзя: они меняются каждый день, и копии будут вечно отставать.

урок из курса

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

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