В каталоге цену товара читают в тысячи раз чаще, чем изменяют. При каких условиях стоит денормализовать цену в модель чтения?
Денормализация оправдана, если частые чтения требуют объединения нескольких таблиц или дорогих вычислений, а допустимая задержка распространения изменения цены заранее определена. В модель чтения копируют цену вместе с данными каталога и обновляют её при изменении исходной записи.
Главный компромисс — более быстрые и дешёвые чтения против усложнения записи, контроля согласованности и риска временно устаревшей цены. Для операций, где цена должна быть строго актуальной, например для окончательного списания денег, модель чтения нельзя считать единственным источником истины.
В нормализованной схеме цена обычно хранится в одном месте, чтобы избежать дублирования и аномалий обновления. Такой подход упрощает поддержание целостности, но чтение страницы каталога может требовать соединений таблиц, обращения к нескольким индексам или вычисления актуальной цены.
В системах с преобладанием чтений возник подход денормализации: часто используемые данные копируют в форму, оптимизированную под конкретный запрос. При дальнейшем росте нагрузки эту идею часто оформляют как отдельную модель чтения или материализованное представление.
Если каждый запрос каталога получает цену через сложные соединения, база может тратить ресурсы на одну и ту же работу. Масштабирование чтений репликами уменьшит нагрузку на основной экземпляр, но не устранит стоимость запроса и может добавить задержку репликации.
Копирование цены создаёт обратный риск: источник уже обновлён, а строка каталога ещё содержит старое значение. Если такое значение использовать для оформления заказа, клиенту можно показать одну цену, а списать другую; поэтому граница допустимой несогласованности должна быть частью требований.
Сначала определяют источник истины и требования к чтению. Основная цена хранится в нормализованной модели товара или прайс-листа, а в модели каталога появляется копия, подготовленная для быстрого поиска и отображения.
При изменении исходной цены система публикует событие или иным способом запускает обновление модели чтения. Обработчик изменяет копию идемпотентно и должен учитывать порядок версий: более старое событие не должно перезаписать результат более нового.
Денормализация особенно полезна, когда:
Нужно измерять не только RPS, но и стоимость одного чтения, задержку обновления копии, долю устаревших записей и число неуспешных обновлений. Для защиты от рассинхронизации применяют версии записи, повторную обработку событий и периодическую сверку модели чтения с источником.
Для корзины или страницы каталога устаревшая цена может быть приемлема, если интерфейс явно обновляет её перед подтверждением. Для расчёта заказа и платежа цена должна проверяться по авторитетному источнику в рамках согласованной бизнес-операции; отображаемая копия не должна использоваться как доказательство итоговой суммы.
Компромисс также касается записи: изменение одной цены теперь требует обновить не только источник, но и зависимые копии. Если цена присутствует в миллионах вариантов выдачи, стоимость fan-out-обновления может превысить выгоду от денормализации; тогда лучше хранить цену в меньшем числе моделей или собирать её при чтении.
В маркетплейсе страница категории выполняла соединение товара, текущего прайс-листа и правил региона. При большом трафике задержка чтения росла, хотя цены менялись относительно редко.
Рассматривались три варианта. Оптимизация соединений и индексов сохраняла единственный источник истины, но имела ограниченный эффект. Кэширование готовых страниц уменьшало нагрузку, однако плохо работало при частичных изменениях и усложняло инвалидацию. Денормализованная модель каталога увеличивала сложность обновления, зато позволяла читать страницу из одной подготовленной структуры.
Выбрали модель чтения с версией цены и асинхронным обновлением. На странице каталога допускалась кратковременная задержка, но перед подтверждением заказа цена повторно проверялась в сервисе ценообразования. Это разделило дешёвое отображение и строгое бизнес-решение, не превращая копию в источник истины.
Ответ: Нужно заранее задать измеримую гарантию, например максимальный возраст копии или допустимую долю чтений с устаревшим значением. Затем измерять время от фиксации изменения в источнике до применения его в модели чтения, включая задержку очереди, обработку и повторные попытки. Среднее значение недостаточно: важны хвостовые задержки и поведение при сбоях.
Ответ: Более старое событие может перезаписать новую цену, создав длительно неверную копию. Поэтому событие или запись должны содержать монотонную версию, время действия с чёткой семантикой либо другой порядковый признак. Обработчик принимает обновление только если его версия не старше уже применённой; повторная доставка при этом должна быть безопасной.
Ответ: Транзакция базы данных обычно охватывает только операции внутри одного хранилища и не делает отдельный обработчик или другую систему частью той же атомарной операции. После фиксации источника процесс обновления копии может завершиться с ошибкой или быть временно недоступен. Поэтому нужны надёжная доставка изменения, повторная обработка, наблюдаемость и механизм сверки; при требовании строгой немедленной согласованности копию следует обновлять в той же транзакционной границе или не использовать её для критического решения.