Основы
5 мин чтения·

Нормализация баз данных: 1НФ, 2НФ и 3НФ на одном примере

Нормализация — это приведение структуры базы данных к виду, в котором каждый факт хранится ровно один раз. Правила нормализации описаны ступенями — нормальными формами: каждая следующая форма устраняет свой тип дублирования. На практике в подавляющем большинстве случаев достаточно первых трёх.

Разберём все три формы на одном сквозном примере, который пройдёт путь от «таблицы-свалки» до нормальной схемы.

Исходная точка: всё в одной таблице

Служба доставки еды хранит заказы так:

order_idcustomerphoneitemsprices
1Анна+7 915 000-11-22Пицца, Кола12.99, 2.49
2Борис+7 903 555-66-77Суши-сет24.99
3Анна+7 915 000-11-22Кола, Чипсы2.49, 2.99

Выглядит компактно, но работать с этим невозможно:

  • Посчитать продажи колы нельзя — товары склеены в строку, остаётся только LIKE '%Кола%' и надежда, что не появится «Кола Зеро».
  • У Анны сменился телефон — обновлять придётся все её заказы. Пропустили строку — в базе два разных телефона, и какой верный, уже не выяснить.
  • Удалили заказ 2 — вместе с ним из базы исчез Борис с его телефоном.
  • Новый клиент без заказа — записать его некуда.

Эти ситуации называют аномалиями изменения, вставки и удаления. Нормализация существует ровно для того, чтобы их не было.

1НФ: в одной ячейке — одно значение

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

order_idcustomerphoneproductprice
1Анна+7 915 000-11-22Пицца12.99
1Анна+7 915 000-11-22Кола2.49
2Борис+7 903 555-66-77Суши-сет24.99
3Анна+7 915 000-11-22Кола2.49
3Анна+7 915 000-11-22Чипсы2.99

Продажи колы теперь считаются обычным GROUP BY. Строку однозначно определяет пара (order_id, product) — это составной ключ таблицы.

Но дублирования стало даже больше: имя и телефон Анны повторяются в каждой строке каждого её заказа. Идём дальше.

В таблице сотрудников есть столбец skills со значениями вида 'SQL, Python, Excel'. Какое правило нормализации нарушено?

2НФ: факт зависит от всего ключа, а не от его части

Вторая нормальная форма смотрит на таблицы с составным ключом и требует: каждый неключевой столбец должен зависеть от всего ключа (order_id, product), а не от его части. У нас два нарушения:

  • customer и phone зависят только от order_id — кто сделал заказ, не меняется от того, какой товар в строке;
  • price зависит только от product — цена пиццы одна и та же во всех заказах.

Выносим каждый факт в таблицу, где он зависит от своего ключа целиком:

MySQL 8.1
CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer VARCHAR(100),
    phone VARCHAR(20)
);

CREATE TABLE products (
    product_id INT PRIMARY KEY,
    name VARCHAR(100),
    price DECIMAL(10,2)
);

CREATE TABLE order_items (
    order_id INT REFERENCES orders(order_id),
    product_id INT REFERENCES products(product_id),
    quantity INT,
    PRIMARY KEY (order_id, product_id)
);

Цена товара теперь хранится один раз в products, состав заказа — компактными парами идентификаторов в order_items.

3НФ: неключевые столбцы не зависят друг от друга

Третья нормальная форма добивает последнее дублирование — транзитивные зависимости, когда неключевой столбец зависит не от ключа, а от другого неключевого столбца. В таблице orders осталось ровно это: phone зависит от customer, а не от номера заказа. Телефон Анны всё ещё повторяется в каждом её заказе.

Выносим клиента в собственную таблицу:

MySQL 8.1
CREATE TABLE customers (
    customer_id INT PRIMARY KEY,
    name VARCHAR(100),
    phone VARCHAR(20)
);

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT REFERENCES customers(customer_id),
    order_date TIMESTAMP
);

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

В таблице заказов хранятся столбцы city и city_zip, причём индекс однозначно определяется городом. Какая форма нарушена?

Итоговая схема

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

Проверим, что стало с аномалиями: телефон Анны меняется в одном месте; удаление заказа не трогает клиента; клиент без заказа спокойно живёт в customers; продажи колы считаются по order_items без всяких LIKE.

Путь нормализации: от таблицы-свалки к трём нормальным формам

Когда дублирование оправдано: денормализация

Нормализация — не самоцель, а способ защитить данные от рассинхронизации. Иногда от неё осознанно отступают:

  • Снимок момента. Цена в products меняется, но в чеке оплаченного заказа она должна остаться той, какой была при покупке. Поэтому в order_items часто добавляют столбец price — это не ошибка, а фиксация исторического факта.
  • Готовые итоги. Сумму заказа можно каждый раз считать по позициям, но при тысячах запросов в секунду дешевле хранить total_amount в orders и обновлять при изменении состава.
  • Аналитика. В отчётных витринах данные намеренно «складывают обратно» в широкие таблицы, чтобы запросы не собирали десяток соединений.

Ключевое слово — осознанно: денормализация всегда означает, что за совпадение дублей теперь отвечает ваш код, а не СУБД.

Что дальше

Читайте по теме