Learning Platform
Глоссарий Troubleshooting
Урок 09.05 · 18 мин
Начальный
denormalizationperformanceredundancydata-modeling

Денормализация: когда и зачем нарушать нормальные формы

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

Звучит как противоречие, но это не так. Денормализация — не отмена нормализации и не «плохой дизайн». Это инженерный компромисс с открытыми глазами: вы знаете, какую цену платите, и принимаете её осознанно. Ключевые слова — «сознательно» и «после». Сначала нормализуйте, поймите модель, и только потом, при доказанной необходимости, точечно денормализуйте.


Почему нормализация имеет цену на чтении

У нормализации есть оборотная сторона. Раскладывая данные так, чтобы каждый факт хранился один раз, мы разносим связанные данные по разным таблицам. А значит, чтобы собрать их обратно для запроса, нужен JOIN.

Вспомним результат из урока про 3NF — пять таблиц:

CITIES, CUSTOMERS, ORDERS, PRODUCTS, ORDER_ITEMS

Простой бизнес-вопрос «покажи позиции заказа с названием товара, именем покупателя и его городом» требует соединить четыре таблицы:

SELECT o.order_id, p.product_name, c.customer_name, ct.city_name, oi.qty
FROM order_items oi
JOIN orders   o  ON o.order_id     = oi.order_id
JOIN products p  ON p.product_id   = oi.product_id
JOIN customers c ON c.customer_id  = o.customer_id
JOIN cities   ct ON ct.city_code   = c.city_code;

Каждый JOIN — это работа: сопоставление строк по ключу, обращения к индексам, промежуточные наборы строк в памяти. Для OLTP с точечными запросами это незаметно — соединить несколько строк по индексу мгновенно. Но есть сценарии, где цена JOIN становится ощутимой: аналитический запрос соединяет десятки таблиц на миллионах строк; высоконагруженный endpoint выполняет один и тот же многотабличный JOIN тысячи раз в секунду; нужны предвычисленные агрегаты, которые иначе пересчитываются каждый раз с нуля.

В этих случаях возникает соблазн: сохранить связанные данные рядом, чтобы убрать JOIN. Это и есть денормализация.

Нормализация и денормализация — противоположные движения
ДенормализованнаяСвязанные данные в одной таблице: быстрое чтение, но избыточность и аномалии
нормализация: убрать избыточность
НормализованнаяКаждый факт один раз: нет аномалий, но чтение требует JOIN

Паттерны денормализации

Денормализация — не «свалить всё в одну таблицу», а набор конкретных приёмов. Разберём основные.

1. Дублирование атрибута (предрассчитанный JOIN). Часто запрашиваемый атрибут из родительской таблицы копируют в дочернюю, чтобы не делать JOIN. Например, в ORDER_ITEMS добавляют product_name рядом с product_id:

ORDER_ITEMS(order_id, product_id, product_name, qty)   -- product_name продублирован

Запрос «позиции заказа с названиями» теперь не соединяется с PRODUCTS. Цена — product_name снова дублируется в каждой строке заказа с этим товаром, и переименование товара требует обновления всех этих строк (тот самый update anomaly из начала модуля — но теперь принятый сознательно).

2. Предвычисленный агрегат. Результат агрегирующего запроса хранят как столбец, а не считают каждый раз. Например, в ORDERS добавляют total_amount — сумму по всем позициям заказа:

ORDERS(order_id, customer_id, total_amount)   -- = SUM(qty * unit_price) по позициям

Чтение «сумма заказа» становится одним обращением к столбцу вместо агрегации по ORDER_ITEMS. Цена — при каждом изменении позиции заказа total_amount надо пересчитывать, иначе он рассинхронизируется с реальными позициями.

3. Счётчик / поддерживаемое значение. Частный случай агрегата — хранить число связанных строк: customers.orders_count вместо COUNT(*) по ORDERS. Та же логика: быстрое чтение, цена — синхронное обновление при каждом новом заказе.

4. Объединение таблиц «1:1». Две таблицы, связанные один-к-одному, сливают в одну, убирая JOIN между ними. Это самый «безобидный» приём — при строгом 1:1 избыточность не возникает, исчезает лишь сам JOIN.

ПаттернЧто ускоряетЧем платим
Дублирование атрибутаУбирает JOIN с родительской таблицейUpdate anomaly по дублю
Предвычисленный агрегатУбирает агрегацию на чтенииПересчёт при каждом изменении источника
Счётчик связанных строкУбирает COUNTСинхронное обновление счётчика
Слияние 1:1 таблицУбирает JOIN между нимиПочти ничего (при строгом 1:1)

Цена денормализации

Денормализация платит ровно той монетой, против которой боролся весь модуль. Цена складывается из нескольких частей.

Возврат аномалий. Каждый продублированный атрибут или агрегат — это факт, хранящийся более одного раза, то есть потенциальный update/insert/delete anomaly. Денормализованная схема снова допускает рассинхрон.

Стоимость поддержания согласованности перекладывается на запись. В нормализованной схеме согласованность бесплатна — её обеспечивает структура. В денормализованной за неё надо платить на каждой записи: при изменении товара — обновить все его копии; при изменении позиции — пересчитать total_amount. Запись становится медленнее и сложнее. Денормализация — это сделка «быстрее чтение в обмен на медленнее и сложнее запись».

Риск тихого расхождения. Самое опасное. Если хоть один путь изменения данных забудет обновить дубль или агрегат, данные молча разойдутся. total_amount будет показывать одно, сумма позиций — другое, и СУБД об этом не предупредит: для неё это просто разные столбцы. Чем больше точек записи, тем выше риск.

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

Денормализация: сделка чтение-vs-запись
ВыигрышЧтение быстрее: меньше JOIN, агрегаты уже посчитаны
в обмен на
ПлатаЗапись медленнее и сложнее: надо синхронно обновлять дубли и агрегаты
и риск
РискЕсли механизм синхронизации даст сбой — данные тихо разойдутся

Механизмы синхронизации: чем платить за денормализацию

Раз денормализация перекладывает согласованность на запись, у дублей и агрегатов должен быть механизм, который их обновляет. Денормализация без такого механизма — это не оптимизация, а просто баг, который проявится при первом расхождении данных. Разберём основные варианты.

1. Транзакция приложения. Код, меняющий источник, в той же транзакции обновляет и дубль. Добавили позицию заказа — этой же транзакцией пересчитали orders.total_amount. Плюс: обновление атомарно — либо оба изменения, либо ни одного, рассинхрон внутри одной транзакции невозможен. Минус: дисциплина — каждый путь записи в источник обязан помнить про дубль; забыл один путь — расхождение.

2. Триггер базы данных. СУБД сама при изменении источника запускает код, обновляющий дубль. Триггер на INSERT/UPDATE/DELETE в order_items пересчитывает total_amount. Плюс: работает на всех путях записи без исключения — логика в самой базе, обойти нельзя. Минус: логика «прячется» в БД, её сложнее увидеть и отлаживать, и триггер добавляет работу на каждую запись.

3. Регламентный пересчёт. Дубль или агрегат периодически пересчитывается с нуля (по расписанию, фоновой задачей). Плюс: просто, и пересчёт «лечит» любой накопленный рассинхрон. Минус: между пересчётами данные временно расходятся — подходит только там, где такое отставание терпимо.

МеханизмГарантия согласованностиЦена
Транзакция приложенияТочная, но только на учтённых путях записиДисциплина: каждый путь обязан обновлять дубль
Триггер БДТочная, на всех путях записиСкрытая логика, нагрузка на каждую запись
Регламентный пересчётПриблизительная, с отставаниемВременный рассинхрон между прогонами
Денормализация требует механизма синхронизации
Источник изменилсяИзменилась позиция заказа или атрибут товара
механизм синхронизации
Дубль / агрегат обновлёнТранзакция, триггер или регламентный пересчёт приводят дубль в соответствие

Выбор механизма — часть проектирования денормализации. Нужна строгая согласованность здесь и сейчас (баланс счёта) — транзакция или триггер. Допустимо небольшое отставание (аналитический счётчик на дашборде) — подойдёт регламентный пересчёт, он проще. Но какой-то механизм обязателен всегда: денормализованный дубль без сопровождающего его процесса синхронизации гарантированно разойдётся с источником.

WARNING

Решая денормализовать, сразу проектируйте механизм синхронизации — это не отдельная задача «на потом», а неотъемлемая часть денормализации. Дубль, у которого нет ясного ответа на вопрос «кто и когда его обновляет», — это запланированный рассинхрон.

Когда денормализация оправдана

Денормализация — инструмент, а не стиль. Применять её стоит при совпадении нескольких условий:

  • Доказанная проблема производительности. Не «кажется, что JOIN медленный», а измеренный, попавший в профиль запрос, который не ускоряется индексами и переписыванием. Денормализация — не первое средство, а одно из последних.
  • Перекос чтения над записью. Данные читаются на порядки чаще, чем меняются. Тогда «дорогая запись ради дешёвого чтения» — выгодный размен. Если данные меняются часто, денормализация может всё замедлить.
  • Есть надёжный механизм синхронизации. Триггеры, транзакционное обновление или регламентный пересчёт — на месте и протестированы.
  • Цена рассинхрона приемлема. Для аналитического счётчика, который раз в сутки пересчитывается, небольшое временное расхождение терпимо. Для финансового баланса — нет; там денормализация требует особой строгости.

И главный архитектурный принцип:

TIP

Нормализуйте сначала, денормализуйте потом — и только точечно. Сперва спроектируйте корректную нормализованную схему (3NF): вы поймёте структуру данных и все зависимости. Денормализуйте после, по результатам измерений, на конкретных горячих местах. Денормализация поверх непонятой модели — это не оптимизация, а узаконенный беспорядок.


Денормализация в OLTP и в аналитике

Важно различать два контекста — об одном из них пойдёт речь дальше в курсе.

В OLTP (системы, где пользователи правят данные в реальном времени) денормализация — это всегда исключение, точечная мера под доказанную проблему. Базовая схема OLTP — нормализованная (3NF): целостность здесь критична, а данные постоянно меняются.

В аналитических хранилищах (OLAP, data warehouse) ситуация переворачивается. Там денормализация — не исключение, а норма проектирования. Размерные модели (star schema), широкие таблицы (One Big Table) намеренно денормализованы: данные грузятся контролируемым процессом, почти не меняются после загрузки, а читаются тяжёлыми аналитическими запросами, для которых десятки JOIN — это смерть. В этом контексте за согласованность отвечает процесс загрузки (ETL/ELT), а не структура. Этой теме — почему аналитике нужна другая модель и как её строят — посвящены следующие модули курса.

Денормализация в PostgreSQL: tradeoffs и паттерны Денормализация в ClickHouse: широкие таблицы как норма
WARNING

«Денормализация = плохой дизайн» — распространённое заблуждение. В OLTP неконтролируемая избыточность действительно дефект. Но в аналитическом слое осознанная денормализация — правильное, профессиональное решение. Разница не в самой избыточности, а в том, осознана ли она, есть ли механизм синхронизации, и подходит ли она под паттерн нагрузки. Денормализация — это про контекст.


Попробуй сам

Дана нормализованная схема интернет-магазина: CUSTOMERS, ORDERS, ORDER_ITEMS, PRODUCTS. Известно: страница «история заказов покупателя» открывается очень часто и каждый раз показывает по каждому заказу его итоговую сумму и число позиций; заказы после оформления почти никогда не меняются.

Выполните на бумаге:

  1. Предложите две денормализации, которые ускорят страницу «история заказов». Для каждой укажите, какой именно JOIN или какую агрегацию она убирает.
  2. Для каждой денормализации опишите цену: какой факт станет избыточным и какой anomaly при этом возвращается.
  3. Опишите механизм синхронизации для каждой денормализации: когда и как обновлять предрассчитанное значение, чтобы оно не разошлось с источником.
  4. Объясните, почему условие «заказы после оформления почти не меняются» делает эту денормализацию оправданной, и что изменилось бы, если бы позиции заказа правились постоянно.

Проверка знанийKnowledge check
Что такое денормализация, какова её цена, и почему её следует применять только после нормализации и точечно?
ОтветAnswer
Денормализация — это сознательное добавление избыточности в уже нормализованную схему ради ускорения чтения. Она нужна, потому что у нормализации есть оборотная сторона: раскладывая каждый факт в одно место, она разносит связанные данные по разным таблицам, и сборка их обратно требует JOIN, а предвычисленные агрегаты приходится считать заново при каждом запросе. Основные паттерны: дублирование часто запрашиваемого атрибута из родительской таблицы в дочернюю (убирает JOIN), хранение предвычисленного агрегата как столбца (убирает агрегацию на чтении), счётчик связанных строк, слияние таблиц 1:1. Цена денормализации — это ровно то, против чего боролась нормализация: возвращаются insert/update/delete-аномалии, потому что факт снова хранится более одного раза; стоимость поддержания согласованности перекладывается на запись — каждое изменение источника требует синхронного обновления всех дублей и агрегатов, запись становится медленнее и сложнее; появляется риск тихого рассинхрона, если какой-то путь записи забудет обновить дубль. Поэтому денормализация требует контролируемого механизма синхронизации (триггеры, транзакции, регламентный пересчёт). Применять её следует только после нормализации и точечно по двум причинам. Во-первых, сначала надо спроектировать корректную нормализованную схему, чтобы понять структуру данных и все зависимости — денормализация поверх непонятой модели это не оптимизация, а узаконенный беспорядок. Во-вторых, денормализация оправдана лишь при совпадении условий: доказанная измерением проблема производительности, сильный перекос чтения над записью, наличие надёжного механизма синхронизации и приемлемая цена возможного рассинхрона. Поэтому это точечная мера на конкретных горячих местах, а не стиль проектирования (хотя в аналитических хранилищах денормализация, наоборот, норма — там данные меняет контролируемый процесс загрузки).

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что такое денормализация и зачем она применяется?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5