Нормальные формы и осознанная денормализация

«Расскажите про нормальные формы» — вопрос-фильтр: заученное определение слышно сразу, а понимание проверяется просьбой привести пример.

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

Вопрос 1. Объясните первые три нормальные формы простыми словами

Что проверяют. Можете ли вы объяснить их без учебника — например, будущему стажёру. Формальные определения через функциональные зависимости знать полезно, но интервьюер обычно ждёт понятного объяснения плюс пример нарушения.

  • 1НФ. В ячейке — одно атомарное значение, нет повторяющихся групп столбцов. Нарушение: колонка phones со значением «+7999…, +7495…» или столбцы phone1, phone2, phone3.
  • 2НФ. Выполнена 1НФ, и каждый неключевой атрибут зависит от всего составного ключа, а не от его части. Нарушение: в таблице «позиция заказа» с ключом (order_id, product_id) лежит product_name — он зависит только от product_id.
  • 3НФ. Выполнена 2НФ, и неключевые атрибуты не зависят друг от друга. Нарушение: в таблице «сотрудник» хранятся department_id и department_name — название зависит от отдела, а не от сотрудника.

Бытовая формулировка, которую любят: «каждый атрибут зависит от ключа, от всего ключа и ни от чего, кроме ключа». Это ровно 1–3НФ одной фразой.

Что происходит без нормализации: три аномалии

Возьмём ненормализованную таблицу и посмотрим на проблемы. Запустите пример — он самодостаточен:

-- НЕНОРМАЛИЗОВАННО: данные об отделе продублированы в каждой строке
CREATE TABLE employee_flat (
  id              INTEGER PRIMARY KEY AUTOINCREMENT,
  name            TEXT NOT NULL,
  department_id   INTEGER NOT NULL,
  department_name TEXT NOT NULL,
  department_head TEXT NOT NULL
);

INSERT INTO employee_flat (name, department_id, department_name, department_head) VALUES
  ('Анна',  10, 'Продажи',    'Иванов'),
  ('Борис', 10, 'Продажи',    'Иванов'),
  ('Вера',  20, 'Разработка', 'Сидорова');

-- аномалия ИЗМЕНЕНИЯ: переименовали отдел только в одной строке
UPDATE employee_flat SET department_name = 'Сбыт' WHERE id = 1;

-- теперь у отдела 10 ДВА разных названия
SELECT department_id, department_name, COUNT(*) AS people
FROM employee_flat
GROUP BY department_id, department_name;

Запустите блок: у отдела с department_id = 10 окажется два разных названия — «Сбыт» и «Продажи». Данные противоречат сами себе, и никакой запрос уже не скажет, какое название верное.

Три классические аномалии:

  • Аномалия изменения: факт хранится в N строках, обновили не все — данные разошлись (то, что мы только что воспроизвели).
  • Аномалия вставки: нельзя завести отдел, пока в него не принят хотя бы один сотрудник, — некуда положить строку.
  • Аномалия удаления: уволили последнего сотрудника отдела — вместе с ним исчезли сведения о самом отделе.

Нормализованный вариант убирает все три:

CREATE TABLE department (
  id    INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  head  TEXT NOT NULL
);

CREATE TABLE employee (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  name          TEXT NOT NULL,
  department_id INTEGER NOT NULL REFERENCES department(id)
);

INSERT INTO department (title, head) VALUES ('Продажи','Иванов'), ('Разработка','Сидорова');
INSERT INTO employee (name, department_id) VALUES ('Анна',1), ('Борис',1), ('Вера',2);

-- название отдела теперь ровно в одном месте
UPDATE department SET title = 'Сбыт' WHERE id = 1;

SELECT e.name, d.title AS department, d.head
FROM employee e JOIN department d ON d.id = e.department_id
ORDER BY e.name;

Теперь переименование — одна строка, и разойтись данные не могут физически.

Вопрос 2. Когда вы намеренно денормализуете?

Что проверяют. Понимаете ли, что нормализация — не догма, и можете ли назвать цену решения. Ответ «денормализация — это плохо» так же слаб, как «нормализация всегда».

Обоснованные случаи:

  • Историчность. Цена в позиции заказа, адрес доставки, реквизиты в счёте, тариф на момент подключения. Это не дубль, а снимок факта на момент события: справочник изменится, а документ обязан остаться прежним. Самый частый и самый правильный случай.
  • Витрины и отчётность. Аналитические таблицы специально «плоские», чтобы не соединять десяток таблиц на каждый отчёт.
  • Тяжёлые агрегаты. Счётчик комментариев или сумма заказа, посчитанные заранее, когда пересчёт на лету стоит дорого.
  • Кэш чужих данных. Копия названия контрагента из внешней системы, чтобы не ходить в неё на каждый экран.

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

Требование к денормализованному полю

R-330. Поле orders.items_count — производное, равно количеству
       строк order_item для заказа.
       Обновляется в той же транзакции, что и изменение состава.
       Источник истины — order_item.
       Ежесуточно в 03:00 выполняется сверка; расхождения
       фиксируются в журнале и исправляются пересчётом.

Вопрос 3. Как хранить исторические данные, если справочник меняется?

Быстрый и сильный ответ — назвать варианты и выбрать. Первый: копировать значение в документ (цена, адрес, наименование на момент операции) — просто и надёжно для документов. Второй: версионировать сам справочник, добавив период действия (valid_from, valid_to) — нужен, когда важно уметь восстановить состояние справочника на любую дату, например для тарифов. Третий: журнал изменений отдельной таблицей — когда важен аудит «кто и когда поменял».

Выбор объясняется вопросом к бизнесу: «Если завтра поменяется название товара, что должно быть в чеке за прошлый месяц — старое или новое?» Ответ на этот вопрос и определяет модель.

Типичные ошибки кандидатов

  • Пересказывают определения НФ, но не могут привести пример нарушения — сразу видно заучивание.
  • Считают денормализацию ошибкой в любом виде и предлагают вычислять цену заказа из текущего прайса.
  • Денормализуют без указания источника истины и правил сверки.
  • Путают 2НФ и 3НФ: 2НФ имеет смысл только при составном ключе.
  • Считают, что 1НФ нарушается любым JSON-полем. Хранить документ в JSON осознанно — нормально, если по его содержимому не строится реляционная логика.

Как ответить кратко

Нормализация — про то, чтобы каждый факт хранился в одном месте: «каждый атрибут зависит от ключа, от всего ключа и ни от чего, кроме ключа» — это 1–3НФ одной фразой. Без неё получаем три аномалии: изменения, вставки и удаления, — например, отдел переименовали в одной строке из трёх, и данные разошлись. Денормализую осознанно в четырёх случаях: историчность документа, витрины отчётности, дорогие агрегаты, кэш внешних данных. Причём историчность — это не дубль, а снимок факта на момент операции: цена в заказе обязана остаться прежней при изменении прайса. Плата за денормализацию — ручная поддержка согласованности, поэтому в требовании фиксирую источник истины, момент обновления и регламент сверки.

Проверьте себя
1. В таблице сотрудников хранятся поля department_id и department_name. Какая нормальная форма нарушена?
AПервая — значение не атомарно
BВторая — атрибут зависит от части составного ключа
CТретья — неключевой атрибут зависит от другого неключевого
DНи одна, это корректная структура
2. Почему цену товара копируют в позицию заказа, хотя это выглядит как дублирование?
AТак быстрее работают отчёты, других причин нет
BЭто снимок факта на момент покупки: цена в прайсе изменится, а сумма оформленного заказа обязана остаться прежней
CИначе нарушается вторая нормальная форма
DЧтобы не делать соединение таблиц при выводе корзины
3. Что обязательно описать в требовании к денормализованному (производному) полю?
AТип данных и длину поля
BИндексы, которые нужно по нему построить
CИсточник истины, момент обновления и регламент сверки расхождений
DНазвание таблицы в физической модели