Урок 02

Нормализация: 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: «ключ, весь ключ, ничего кроме ключа».
  • Денормализация — осознанно.
  • Аномалии: вставки, обновления, удаления.

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