Нормализация: 1NF, 2NF, 3NF
Что такое нормализация
Нормализация — процесс приведения схемы БД к форме, устраняющей избыточность и аномалии.
Аномалии:
- Вставки — нельзя вставить без другого.
- Обновления — надо менять в N местах.
- Удаления — удаление одного случайно удаляет другое.
Что даёт:
- Нет дублей.
- Нет противоречий.
- Проще изменения.
- Меньше размер БД.
Что стоит:
- Больше таблиц.
- Больше JOIN.
- Иногда медленнее чтение.
Функциональная зависимость
A → B — если знаем A, знаем B.
Пример:
user_id → email
email → user_id
order_id → user_id
order_id, product_id → quantity
Виды:
- Полная — от всего ключа.
- Частичная — от части составного ключа.
- Транзитивная — через промежуточный атрибут (
A → B → C).
Первая нормальная форма (1NF)
Требования:
- Атомарные значения в ячейках.
- Нет массивов, списков, вложенных структур.
- Каждая колонка — одно скалярное значение.
- Уникальные имена колонок.
- Порядок строк не важен.
❌ Нарушение:
| id | name | phones |
|----|-------|-------------------------|
| 1 | Алиса | +79990001122, +79990003344 |
Проблемы:
- Нельзя найти по конкретному телефону.
- Нельзя добавить третий без обновления строки.
- Нельзя удалить один.
✅ 1NF:
| id | name | phone |
|----|-------|---------------|
| 1 | Алиса | +79990001122 |
| 1 | Алиса | +79990003344 |
Или через отдельную таблицу:
CREATE TABLE user_phones (
user_id BIGINT NOT NULL,
phone VARCHAR(20) NOT NULL,
PRIMARY KEY (user_id, phone)
);
Что ещё нарушает 1NF:
- JSON с массивами — если по элементам нужен поиск.
- CSV-строки —
'php,laravel,js'. - Повторяющиеся колонки —
phone1, phone2, phone3. - Составные значения —
'Москва, ул. Ленина, 5'.
⚠️ PostgreSQL-массивы (TEXT[]) формально нарушают 1NF. Оправдано, если по ним не ищут.
Вторая нормальная форма (2NF)
Требования:
- Уже 1NF.
- Нет частичных зависимостей — атрибут зависит от всего составного ключа, а не от части.
❌ Нарушение:
order_items
| order_id | product_id | quantity | product_name | product_price |
|----------|------------|----------|--------------|---------------|
| 1 | 100 | 2 | iPhone | 80000 |
PK: (order_id, product_id).
Проблема: product_name и product_price зависят только от product_id, не от всего ключа. Это частичная зависимость.
Что плохо:
- Дублирование (название повторяется для каждого заказа).
- Обновление цены — надо менять во всех строках.
- Аномалия вставки — нельзя добавить товар без заказа.
✅ 2NF:
products
| id | name | price |
|-----|---------|-------|
| 100 | iPhone | 80000 |
order_items
| order_id | product_id | quantity |
|----------|------------|----------|
| 1 | 100 | 2 |
Правило: если ключ составной — все неключевые атрибуты зависят от всего ключа.
⚠️ Если PK — одна колонка, 2NF всегда выполняется (если 1NF).
Третья нормальная форма (3NF)
Требования:
- Уже 2NF.
- Нет транзитивных зависимостей — неключевой атрибут не зависит от другого неключевого.
❌ Нарушение:
orders
| id | user_id | user_email | user_city |
|----|---------|---------------|-----------|
| 1 | 5 | a@b.com | Москва |
PK: id.
Проблема:
id → user_id → user_email → user_city
user_email зависит от user_id, а не от id. Транзитивная зависимость.
Что плохо:
- Дублирование (email для каждого заказа).
- Обновление email — в N строках.
- Противоречия — разные email для одного user_id.
✅ 3NF:
users
| id | email | city |
|----|---------|--------|
| 5 | a@b.com | Москва |
orders
| id | user_id |
|----|---------|
| 1 | 5 |
Правило: неключевые атрибуты зависят только от ключа, не друг от друга.
Мнемоника:
Каждый неключевой атрибут зависит от ключа, всего ключа и ничего, кроме ключа.
Пример: интернет-магазин
❌ Ненормализованная:
| order_id | customer_name | customer_email | product1_name | product1_price | product2_name | product2_price |
Проблемы:
- Повторяющиеся группы (product1, product2) — нарушение 1NF.
- Данные клиента дублируются — нарушение 2NF/3NF.
- Максимум 2 товара — глупо.
✅ 3NF:
CREATE TABLE customers (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE
);
CREATE TABLE products (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
price NUMERIC(10, 2) NOT NULL
);
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customers(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE order_items (
order_id BIGINT NOT NULL REFERENCES orders(id),
product_id BIGINT NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL,
price_at_purchase NUMERIC(10, 2) NOT NULL, -- цена на момент покупки
PRIMARY KEY (order_id, product_id)
);
Особенность: price_at_purchase — намеренная денормализация. Цена товара меняется, а цена покупки — нет. Это бизнес-требование, не нарушение 3NF.
Нормальные формы дальше
BCNF (Boyce-Codd):
- Уже 3NF.
- Каждый детерминант — потенциальный ключ.
- Строже 3NF.
4NF:
- Уже BCNF.
- Нет многозначных зависимостей.
5NF:
- Уже 4NF.
- Нет зависимостей соединения.
6NF:
- Для temporal-БД.
Правило: в реальной работе останавливаются на 3NF / BCNF. 4NF и 5NF — редкость.
Когда НЕ нормализовать
Денормализация оправдана:
- Кэш агрегатов — счётчик заказов в профиле.
- Исторические данные — цена на момент покупки.
- Аналитика — OLAP-кубы, материализованные представления.
- Скорость чтения — редко меняется, часто читается.
Правило: денормализуй осознанно, документируй причину.
Частые ошибки
1. Хранение списков в строке:
tags VARCHAR(255) -- 'php,laravel,js'
Решение: отдельная таблица.
2. Повторяющиеся колонки:
phone1, phone2, phone3
Решение: отдельная таблица.
3. Имя, адрес, телефон в каждом заказе:
Дублируется
Решение: ссылки на справочники.
4. user_id, user_name, user_email в одной таблице:
Транзитивная зависимость
Решение: таблица users.
5. Хранение возраста вместо даты рождения:
age INT -- устаревает
Решение: birth_date DATE.
6. Хранение суммы вместо расчёта:
total NUMERIC -- если есть order_items
Решение: SUM(quantity * price), если не нужен snapshot.
7. id, uuid, email — все уникальны:
Избыточно
Решение: PK + один естественный ключ.
Практика
1. Возьмите таблицу из своего проекта. Проверьте на 1NF.
- Есть ли массивы/списки в колонках?
- Есть ли повторяющиеся колонки?
2. Проверьте на 2NF.
- Есть ли составной PK?
- Все ли атрибуты зависят от всего ключа?
3. Проверьте на 3NF.
- Есть ли транзитивные зависимости?
4. Нормализуйте до 3NF.
5. Задокументируйте намеренные денормализации (если есть).
Итог
- 1NF: атомарные значения, нет списков.
- 2NF: нет частичных зависимостей от составного ключа.
- 3NF: нет транзитивных зависимостей.
- BCNF — строже 3NF.
- Мнемоника 3NF: «ключ, весь ключ, ничего кроме ключа».
- Денормализация — осознанно.
- Аномалии: вставки, обновления, удаления.
Дальше — денормализация: когда и зачем.