Введение
Статья 403 уже разбирала денормализацию подробно — в этой, более короткой статье блока производительности явно сводим воедино компромисс между нормализацией (статья 402, целостность и минимизация избыточности) и денормализацией (производительность чтения за счёт сложности поддержания согласованности), формулируя практические критерии выбора для конкретной ситуации.
Концепция
Решение нормализовать или денормализовать конкретный участок схемы должно опираться на конкретные, измеримые факторы: частоту чтения относительно частоты записи (данные, читаемые значительно чаще, чем изменяемые, — хорошие кандидаты для денормализации), критичность абсолютной согласованности данных (финансовые расчёты обычно требуют строгой нормализации без компромиссов, тогда как отображаемые «примерные» счётчики могут допускать кратковременную неточность), и реально измеренную, а не предполагаемую проблему производительности нормализованного варианта.
Пример кода: Денормализация для производительности
-- Сценарий, благоприятный для денормализации: счётчик "лайков" статьи,
-- читаемый при каждом просмотре страницы (часто), изменяемый при каждом лайке (намного реже)
ALTER TABLE articles ADD COLUMN like_count INTEGER DEFAULT 0;
-- Обновление денормализованного счётчика при добавлении лайка
CREATE OR REPLACE FUNCTION increment_like_count()
RETURNS TRIGGER AS $$
BEGIN
UPDATE articles SET like_count = like_count + 1 WHERE id = NEW.article_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_increment_likes AFTER INSERT ON article_likes
FOR EACH ROW EXECUTE FUNCTION increment_like_count();
-- Сценарий, НЕблагоприятный для денормализации: остаток товара на складе,
-- где абсолютная точность критична (риск продажи несуществующего товара)
-- Здесь лучше вычислять остаток через точный, нормализованный запрос при каждой проверке,
-- либо использовать блокировку (SELECT ... FOR UPDATE, статья 409) для атомарного резервирования
SELECT stock_quantity FROM products WHERE id = 5 FOR UPDATE;
-- Периодическая ПЕРЕСИНХРОНИЗАЦИЯ денормализованного значения —
-- защитная мера на случай накопления небольших рассогласований через триггеры/баги
UPDATE articles a
SET like_count = (SELECT COUNT(*) FROM article_likes al WHERE al.article_id = a.id);
-- запускается периодически (например, раз в сутки через задачу планировщика)
-- для исправления любых накопившихся, не обнаруженных рассогласований
Пояснения к коду
Счётчик лайков статьи — классический пример удачной денормализации: соотношение чтения (каждый просмотр страницы статьи) к записи (отдельный лайк от конкретного пользователя) обычно сильно смещено в сторону чтения, кратковременная неточность счётчика (если, например, триггер на мгновение «опаздывает» относительно параллельного запроса на чтение) обычно некритична для пользовательского восприятия, и вычисление точного значения через COUNT() при каждом просмотре было бы значительно более затратным, чем чтение уже готового, денормализованного значения. Остаток товара на складе — противоположный случай, где денормализация (без должной дополнительной защиты через блокировки) рискует реальными финансовыми потерями (продажа товара, которого физически уже нет), и здесь стоимость более строгого, нормализованного подхода с явной блокировкой полностью оправдана повышенными ставками точности. Периодическая пересинхронизация — практичная защитная мера для денормализованных значений, не требующих абсолютной мгновенной точности, но всё же выигрывающих от периодического исправления любых, возможно накопившихся со временем небольших рассогласований.
Подводные камни
- Применение одного и того же подхода (либо всегда строгая нормализация, либо всегда агрессивная денормализация) ко всем данным проекта без дифференцированного анализа — как показывают два контрастных примера в статье, правильное решение существенно различается в зависимости от конкретных характеристик конкретных данных (соотношение чтения/записи, критичность точности), и единое, универсальное правило «для всего проекта» редко оптимально для всех его частей одновременно.
- Недооценка риска денормализации для данных с финансовыми или иными серьёзными последствиями неточности, поддавшись общему соблазну «ускорить всё, что можно ускорить» без разбора последствий конкретно для критичных данных — денормализация остатков товара, балансов счетов, и аналогичных данных, где неточность может привести к реальным финансовым или репутационным потерям, требует значительно более осторожного подхода (или вовсе отказа от денормализации в пользу нормализованного подхода с явными блокировками), чем для менее критичных, «косметических» счётчиков.
- Отсутствие механизма периодической проверки/исправления рассогласований для денормализованных значений, для которых небольшая временная неточность допустима, но долгосрочное, накопленное рассогласование (из-за редких, не обнаруженных багов в логике поддержания согласованности) уже нежелательно — периодическая, фоновая пересинхронизация (как в третьем примере) — практичная страховка против такого постепенного дрейфа денормализованных данных от их истинного, нормализованного значения.
- Принятие решения о денормализации на основе интуиции или общих рекомендаций «из интернета» без реального измерения производительности нормализованного варианта на собственных, реальных данных — то, что было проблемой производительности для одного конкретного проекта с определённым объёмом и распределением данных, может вовсе не быть проблемой для другого проекта с иными характеристиками, и решение о денормализации (с её неизбежной дополнительной сложностью) должно опираться на собственное, конкретное измерение, а не на общие, не всегда применимые именно к данному случаю рекомендации.