awesome-everything EN
↑ Обратно к восхождению

Базы данных

Нормальные формы, денормализация и почему схемы «прилипают»

Суть 1NF–BCNF простыми словами, когда намеренно денормализовать, ORM-ловушки, незаметно ломающие дисциплину схемы, и соглашения об именовании, которые окупаются годами.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 16 min

Младший инженер хранит город клиента в каждой строке заказа — «удобно для отчётов». Через два года клиент переезжает. Теперь половина исторических заказов показывает старый город, половина — новый. Один и тот же факт живёт в двух местах и они расходятся. Нормализация — дисциплина, которая предотвращает это.

Нормальные формы простыми словами

Нормализация — серия прогрессивно строгих правил разбиения таблиц, чтобы ни один факт не хранился дважды.

1NF — атомарные ячейки. Каждая ячейка должна содержать одно значение. Никаких списков через запятую внутри колонки, никаких повторяющихся групп, никаких массивов под видом колонок (tag1, tag2, tag3). Если ячейку можно распарсить через split(','), таблица нарушает 1NF.

2NF — полная ключевая зависимость. Каждая неключевая колонка должна зависеть от всего primary key, а не от его части. Имеет значение только для составных primary keys. Пример нарушения: таблица order_items(order_id, product_id, product_name)product_name зависит только от product_id, а не от составного ключа. Решение: перенести product_name в таблицу products.

3NF — нет транзитивных зависимостей. Каждая неключевая колонка должна зависеть только от primary key, а не от другой неключевой колонки. Пример нарушения: orders(order_id, customer_id, customer_city)customer_city зависит от customer_id, а не от order_id. Решение: перенести customer_city в таблицу customers (или addresses).

BCNF — каждый детерминант является candidate key. Строже 3NF; имеет значение когда таблица имеет несколько перекрывающихся candidate keys. Редко в продакшне; часто в академических примерах. В практике цельтесь в 3NF; переходите к BCNF только когда можете назвать конкретный сценарий перекрывающихся candidate keys.

Нормальная формаПравилоФорма нарушенияРешение
1NFАтомарные ячейки, нет повторяющихся группСписок через запятую в одной колонкеОтдельная таблица или типизированный массив
2NFПолная ключевая зависимостьНеключевая колонка зависит от части составного PKПеренести в таблицу-владелец
3NFНет транзитивных зависимостейНеключевая колонка зависит от другой неключевойВынести в отдельную таблицу
BCNFКаждый детерминант — candidate keyНесколько перекрывающихся candidate keysДекомпозиция; редко на практике

Суть — не в следовании правилам, а в устранении сценария, когда две строки расходятся об одном факте. Если можно обновить email Анны в трёх таблицах, две из них будут устаревшими через год.

Денормализация: когда и зачем

Нормализация оптимизирует корректность при записи. При чтении она может быть дорогостоящей, потому что ответ на вопрос требует соединения многих таблиц. Денормализация намеренно дублирует данные для избежания JOIN на горячих путях чтения.

Сохранённые агрегаты. Колонка order_total_cents в orders, кешируещая SUM(order_items.unit_price_cents * quantity). Обновляется триггером или кодом приложения при каждой записи позиции. Отчётность обращается к одной колонке вместо агрегирования миллионов строк.

Широкие read-модели. Таблица user_profile, объединяющая имя, email, текущий адрес, количество последних заказов — пополняемая триггерами или кодом из нормализованных источников. Запросы дашборда обращаются к одной строке.

Материализованные пути. Хранение полного пути предков узла дерева как текстовой колонки (/root/section/subsection/), чтобы избежать рекурсивных JOIN при чтении иерархий. Классический паттерн для деревьев комментариев или оргструктур.

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

Почему это работает

Почему 3NF, а не 4NF, 5NF или DKNF? Нормальные формы выше 3NF адресуют многозначные зависимости и зависимости соединений, которые встречаются в крайне академических схемах. Реальные продакшн-таблицы почти никогда не имеют таких форм. 3NF — точка убывающей отдачи: за ней вы разбиваете таблицы, которые не нужно разбивать, добавляете JOIN, которые не улучшают корректность, и замедляете чтение для схем, у которых не было проблем с integrity.

ORM-ловушки, незаметно ломающие дисциплину схемы

ORM абстрагируют SQL, но могут вносить ошибки на уровне схемы.

N+1 запросы. ORM загружает список 100 заказов, затем для каждого заказа отдельно получает клиента. Результат: 101 запрос там, где справился бы 1 JOIN. Во многих ORM это поведение по умолчанию, если не объявить eager-load (include, with, select_related). Импликация для схемы: каждая FK-связь должна быть аннотирована правильной стратегией загрузки по умолчанию; оставить это на неявную lazy-load — значит накапливать невидимые N+1.

Неявная генерация схемы. Многие ORM могут генерировать схему из определения модели. Сгенерированная схема часто не содержит: CHECK ограничений, составных индексов, индекса на FK-колонке, именованных ограничений. Результат — синтаксически корректная схема, нарушающая дисциплину этого урока. Воспринимайте ORM-сгенерированные схемы как отправную точку, а не финальный артефакт; добавляйте ограничения и индексы вручную или через файлы миграций.

Отсутствие обновления updated_at. ORM часто автоматически управляют created_at, но не updated_at. Отсутствие updated_at делает инкрементальную репликацию и аудит невозможными. Принудительно соблюдайте через триггер (BEFORE UPDATE ... SET updated_at = now()) или lifecycle hook ORM; никогда не доверяйте приложению помнить об этом.

Соглашения об именовании, которые окупаются

Они кажутся педантичными; они окупаются в каждом дашборде, каждом скрипте миграции, каждом кросс-командном запросе.

  • Множественное число для таблиц (users, orders, order_items) — единственное число тоже допустимо; выберите один вариант и соблюдайте везде.
  • snake_case — Postgres приводит неэкранированные идентификаторы к нижнему регистру; snake_case — продакшн-дефолт.
  • id как имя primary key в каждой таблице; foreign keys называются <referenced_table_singular>_id (например, user_id, order_id).
  • created_at, updated_at, deleted_at для временных колонок. deleted_at IS NOT NULL — паттерн мягкого удаления.
  • Булевы колонки с префиксом is_, has_, can_ или как понятное свойство (active, не flag).
  • Именование CHECK ограничений — именуйте каждое: CONSTRAINT chk_orders_status CHECK (status IN (...)). Безымянные ограничения выдают непонятные сообщения об ошибках.

Зрелые платформы CI-линтят эти соглашения, чтобы они соблюдались командами, а не запоминались отдельными людьми.

Расставь шаги по порядку

Упорядочьте эти действия по эволюции схемы от самых дешёвых к самым дорогим:

  1. 1 Добавить CHECK ограничение на существующую колонку
  2. 2 Добавить UNIQUE индекс (CREATE INDEX CONCURRENTLY)
  3. 3 Добавить FOREIGN KEY (требует прохода проверки по существующим строкам)
  4. 4 Изменить тип колонки (TEXT → INTEGER) — перезапись таблицы
  5. 5 Разбить JSONB-колонку на типизированные колонки — миграция с backfill
  6. 6 Разбить одну таблицу на две с FK — окно двойной записи, проверка паритета
  7. 7 Изменить primary key — каскадирует через каждый FK и индекс; почти никогда не делается
Викторина

Таблица хранит `orders(order_id, customer_id, customer_city)`. Какую нормальную форму она нарушает и почему?

Выбери лучший вариант

Дашборд отчётности нуждается в общей выручке по workspace, пересчитываемой каждые 5 минут. Выберите реализацию.

Викторина

ORM-сгенерированная схема не имеет именованных CHECK ограничений и индексов на FK-колонках. Какие два конкретных сценария отказа это вызывает в продакшне?

Вспомните перед уходом
  1. 01
    Объясните своими словами, почему 3NF является продакшн-дефолтной нормальной формой, и когда команды обоснованно идут выше (BCNF) или ниже (намеренная денормализация).
  2. 02
    Назовите три конкретных ORM-ловушки, незаметно нарушающих дисциплину схемы, и исправление для каждой.
  3. 03
    Что такое паттерн материализованного пути и когда он лучше рекурсивных CTE для запросов к деревьям?
Итог

Нормальные формы устраняют избыточные факты: 1NF (атомарные ячейки), 2NF (полная ключевая зависимость), 3NF (нет транзитивных зависимостей), BCNF (каждый детерминант — candidate key). Цельтесь в 3NF для продакшн-схем. Денормализуйте намеренно — сохранённые агрегаты, широкие read-модели, материализованные пути — с явным согласованием; никогда без плана синхронизации копий. ORM-сгенерированные схемы пропускают ограничения и индексы; добавляйте их. Соглашения об именовании (snake_case, множественное число, id PK, created_at/updated_at/deleted_at, именованные CHECK) соблюдаются через CI. Решения о схеме «прилипают»; цена неверной нормальной формы выплачивается при миграции, а не при проектировании.

Связанные уроки
встречается в140
Продолжить восхождение ↑JSONB, массивы и когда side table побеждает
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.