Learning Platform
Урок 12.03 · 18 мин
Средний
Normalization1NF2NF3NFBCNFFunctional dependency

Зачем вообще нормализовать

Вы получили ТЗ: «нужна таблица заказов». Самое естественное — собрать всё в одну широкую таблицу:

orders (id, customer_name, customer_email, customer_country, product_name, qty, price, placed_at)

Что не так? Через пару месяцев боя обнаружится:

  • Аномалия обновления: клиент сменил email — нужно обновить его во всех ста его заказах. Забыл хоть один — данные становятся противоречивыми.
  • Аномалия вставки: хотите завести нового клиента, но он ещё не делал заказов — некуда вставить.
  • Аномалия удаления: удалили последний заказ клиента — потеряли всё знание о клиенте.
  • Дублирование: вы храните customer_email тысячу раз вместо одного.

Нормализация
— это набор правил, как разбивать «толстые» таблицы на «тонкие», чтобы эти аномалии исчезли. Правил много (1NF, 2NF, 3NF, BCNF, 4NF, 5NF, 6NF), но в практике 99% случаев — это 3NF.

Функциональная зависимость — главная концепция

Функциональная зависимость
X → Y означает: «если у двух строк одинаковый X, то у них обязательно одинаковый Y».

Примеры:

  • customer_id → customer_email: зная id клиента, однозначно знаем его email.
  • order_id → customer_id, placed_at: зная id заказа, знаем кто и когда его сделал.
  • product_id → product_name, category_id: id товара определяет имя и категорию.

Когда таблица содержит зависимости, которые не «крутятся вокруг» её первичного ключа — это симптом проблемы. Нормализация формализует «крутятся вокруг ключа» через три правила.

Что такое функциональная зависимость

X → Y: одинаковый X гарантирует одинаковый Y. Это не утверждение про данные сейчас, это инвариант, который мы обещаем держать всегда.

customer_id42
customer_email = '[email protected]'
customer_country = 'RU'
Зная customer_id, мы однозначно знаем email и countryЕсли эта зависимость держится — её надо вынести в отдельную таблицу customers

1NF — атомарные значения

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

Нарушение 1NF:

orders
| id | customer | products                          |
|  1 | Аня      | iPhone, USB-кабель, чехол         |
|  2 | Борис    | MacBook, USB-хаб                  |

Здесь products — это список в текстовой ячейке. Чтобы посчитать «сколько раз iPhone заказывали», нужно делать LIKE '%iPhone%' — медленно, хрупко, не индексируется.

В 1NF это превращается в две таблицы:

orders          (id, customer)
order_items     (order_id, product, qty)

Маленькая оговорка: Postgres поддерживает массивы (TEXT[]) и JSONB. Формально с точки зрения 1NF Кодда массивы — нарушение. На практике это удобный инструмент, когда данные внутри ячейки не нужно индексировать или фильтровать построчно (теги, конфиг, профиль). Если нужно — выносите в отдельную таблицу.

2NF — никаких зависимостей от части ключа

Вторая нормальная форма действует только если у таблицы составной первичный ключ. Правило: ни один не-ключевой атрибут не должен зависеть от части ключа — только от ключа целиком.

Классический пример нарушения — таблица позиций заказа с лишним полем:

order_items (order_id, product_id, qty, product_name, product_price)
PRIMARY KEY (order_id, product_id)

Зависимости:

  • (order_id, product_id) → qty — норма, qty уникальна на пару.
  • product_id → product_name, product_price — а вот это проблема! product_name зависит только от product_id, то есть от части ключа. Один и тот же продукт в разных заказах будет иметь одно и то же имя — мы дублируем его и теряем гарантию консистентности.

Лечение — вынести product_name и product_price в products:

products    (id PK, name, price)
order_items (order_id, product_id, qty)  -- price можно сохранить отдельно как unit_price_cents

Заметьте: в order_items мы оставили unit_price_cents — это не нарушение 2NF, потому что это исторический снимок цены на момент заказа, а не «текущая цена товара». Цены меняются, а цена в чеке — нет.

3NF — никаких транзитивных зависимостей

Третья нормальная форма: не-ключевые атрибуты должны зависеть только от ключа, и ни от чего другого.

Самый частый случай нарушения 3NF — наш стартовый пример:

orders (id, customer_name, customer_email, customer_country, placed_at)

Зависимости:

  • id → customer_email — норма (через id мы знаем email).
  • customer_email → customer_name, customer_country — а это уже транзитивная зависимость: email определяет имя и страну, минуя id заказа.

Если у клиента «Аня» сто заказов, мы храним её имя и страну сто раз. Когда Аня переезжает — обновлять сто строк.

Лечение — вынести данные клиента в отдельную таблицу:

customers (id PK, email UNIQUE, name, country)
orders    (id PK, customer_id FK, placed_at)

Теперь id заказа → customer_id, а customer_id → email/name/country живёт в customers. Транзитивности больше нет.

3NF: разбиение orders

Слева — нарушение 3NF: имя и страна клиента дублируются в каждом его заказе. Справа — 3NF: имя и страна живут один раз в customers.

Нарушение 3NForders
id, customer_email, customer_name, customer_country, placed_atemail → name, country — транзитивная зависимость
проблемаимя Ани повторяется в каждом её заказе
3NFcustomers + orders
customersid PK, email UNIQUE, name, country
ordersid PK, customer_id FK, placed_at
результатимя Ани — одна строка, везде ссылки

Запомните неформальное мнемоническое правило для 3NF:

Every non-key attribute must depend on the key, the whole key, and nothing but the key — so help me Codd.

— это шутка из учебников, но она правда работает: «зависит от ключа» = 1NF, «от всего ключа» = 2NF, «и ни от чего больше» = 3NF.

BCNF — крайний случай 3NF

Boyce-Codd Normal Form — это «3NF, доведённая до конца». Разница важна только когда у таблицы есть несколько перекрывающихся

candidate keys
. В 99% реальных схем 3NF и BCNF совпадают.

Канонический пример различия — таблица (student_id, course, instructor), где предполагается, что один инструктор ведёт ровно один курс. Тогда instructor → course, но instructor сам не является ключом. Это валидное 3NF (потому что course — часть candidate key (student_id, course)), но нарушение BCNF.

В реальной работе можно просто всегда стремиться к 3NF и не переживать про BCNF отдельно.

Практика: чиним orders

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

Сначала — анти-паттерн: всё в одной таблице

PostgreSQL

Теперь нормализованная версия:

3NF-версия: три таблицы вместо одной

PostgreSQL

Цена нормализации — нужны JOIN-ы, чтобы собрать «полную» картину. На крупных аналитических запросах это может быть медленнее. Об этом — следующий урок про денормализацию.

Функциональные зависимости и аксиомы Армстронга BCNF: каждая нетривиальная FD имеет superkey слева

Чек-лист

  • Цель нормализации — убрать аномалии вставки/обновления/удаления и дублирование данных.
  • Функциональная зависимость X → Y: «один и тот же X гарантирует один и тот же Y». Это фундамент всех нормальных форм.
  • 1NF: атомарные значения, никаких списков в ячейках.
  • 2NF: при составном ключе ни один не-ключевой атрибут не должен зависеть только от части ключа.
  • 3NF: не-ключевые атрибуты не должны зависеть друг от друга — только от ключа.
  • Мнемоника 3NF: «depends on the key, the whole key, and nothing but the key».
  • BCNF — более строгая версия 3NF. В реальной работе они почти всегда совпадают.
  • Цена нормализации — JOIN-ы. Когда они становятся дорогими — см. следующий урок про денормализацию.
Проверка знанийKnowledge check
Дана таблица employees (id, name, dept_id, dept_name, dept_budget). Это какая нормальная форма? Что не так?
ОтветAnswer
Это 2NF, но не 3NF. Тут есть транзитивная зависимость: id → dept_id → dept_name, dept_budget. То есть dept_name и dept_budget зависят от dept_id, а не от id напрямую. Симптомы — те же самые аномалии: если у отдела изменился бюджет, надо обновлять его во всех сотрудниках; если все сотрудники отдела уволились, информация об отделе теряется. Лечение — вынести в отдельную таблицу departments (id PK, name, budget), а в employees оставить только dept_id REFERENCES departments(id).

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

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

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

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

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

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