Нормализация баз данных: 1НФ, 2НФ и 3НФ на одном примере
Нормализация — это приведение структуры базы данных к виду, в котором каждый факт хранится ровно один раз. Правила нормализации описаны ступенями — нормальными формами: каждая следующая форма устраняет свой тип дублирования. На практике в подавляющем большинстве случаев достаточно первых трёх.
Разберём все три формы на одном сквозном примере, который пройдёт путь от «таблицы-свалки» до нормальной схемы.
Исходная точка: всё в одной таблице
Служба доставки еды хранит заказы так:
Выглядит компактно, но работать с этим невозможно:
- Посчитать продажи колы нельзя — товары склеены в строку, остаётся только LIKE '%Кола%' и надежда, что не появится «Кола Зеро».
- У Анны сменился телефон — обновлять придётся все её заказы. Пропустили строку — в базе два разных телефона, и какой верный, уже не выяснить.
- Удалили заказ 2 — вместе с ним из базы исчез Борис с его телефоном.
- Новый клиент без заказа — записать его некуда.
Эти ситуации называют аномалиями изменения, вставки и удаления. Нормализация существует ровно для того, чтобы их не было.
1НФ: в одной ячейке — одно значение
Первая нормальная форма требует атомарности: на пересечении строки и столбца находится одно неделимое значение, без списков и перечислений. Разворачиваем каждую позицию заказа в отдельную строку:
Продажи колы теперь считаются обычным GROUP BY. Строку однозначно определяет пара (order_id, product) — это составной ключ таблицы.
Но дублирования стало даже больше: имя и телефон Анны повторяются в каждой строке каждого её заказа. Идём дальше.
В таблице сотрудников есть столбец skills со значениями вида 'SQL, Python, Excel'. Какое правило нормализации нарушено?
2НФ: факт зависит от всего ключа, а не от его части
Вторая нормальная форма смотрит на таблицы с составным ключом и требует: каждый неключевой столбец должен зависеть от всего ключа (order_id, product), а не от его части. У нас два нарушения:
- customer и phone зависят только от order_id — кто сделал заказ, не меняется от того, какой товар в строке;
- price зависит только от product — цена пиццы одна и та же во всех заказах.
Выносим каждый факт в таблицу, где он зависит от своего ключа целиком:
MySQL 8.1CREATE 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.1CREATE 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 и обновлять при изменении состава.
- Аналитика. В отчётных витринах данные намеренно «складывают обратно» в широкие таблицы, чтобы запросы не собирали десяток соединений.
Ключевое слово — осознанно: денормализация всегда означает, что за совпадение дублей теперь отвечает ваш код, а не СУБД.
Что дальше
- из чего складывается грамотная структура таблиц — в уроке Структура реляционных баз данных;
- первичные и внешние ключи, на которых держатся связи, — в уроке Ограничения;
- проектированию схем «от бизнес-задачи», включая нормализацию, денормализацию и работу с историей данных, посвящён целый курс — Проектирование БД и архитектура.
Читайте по теме
SQL с нуля: пошаговый план изучения для начинающих в 2026
Дорожная карта на 6 недель — от первого SELECT до собеседования
ACID: 4 свойства транзакций в базах данных простыми словами
Атомарность, согласованность, изолированность, долговечность — на примере перевода денег
ROW_NUMBER, RANK и DENSE_RANK в SQL: отличия на одном примере
Три ранжирующие функции, один запрос — и разница видна