SQLбазы данныхнормализацияпроектирование БДаналитика

Нормализация БД простыми словами: 1НФ, 2НФ, 3НФ

2026-07-10 11 мин

Коротко: нормализация базы данных — это разбиение одной широкой таблицы на несколько связанных так, чтобы каждый факт хранился ровно в одном месте. Делают это, чтобы убрать три типа аномалий: при вставке, обновлении и удалении данных. Классическая цель — третья нормальная форма (3НФ): убрать повторяющиеся группы (1НФ), вынести атрибуты, зависящие лишь от части ключа (2НФ), и убрать транзитивные зависимости между неключевыми полями (3НФ). В OLTP-системах нормализуют до 3НФ, а под аналитику (OLAP) осознанно денормализуют обратно ради скорости чтения.

Зачем вообще нужна нормализация базы данных?

Представь, что заказы интернет-магазина хранятся в одной таблице orders_flat, куда свалили всё: покупателя, его город, товары, цену, категорию. Выглядит удобно — один SELECT * и вся картина перед глазами. Пока таблица маленькая, никто не жалуется.

Проблема в том, что одни и те же факты повторяются в десятках строк. Город покупателя дублируется в каждом его заказе. Название категории товара — в каждой строке, где этот товар встречается. И как только данные начинают повторяться, они начинают расходиться: где-то «Санкт-Петербург», где-то «СПб», где-то «Питер». Отчёт по городам разваливается.

Нормализация решает это одним принципом: один факт — одно место хранения. Город покупателя лежит в таблице customers ровно один раз. Категория товара — в products один раз. Заказ ссылается на них по идентификатору. Меняешь факт — меняешь в одной строке, и он автоматически «правильный» везде.

Вот та самая грязная таблица, на которой мы разберём все три формы:

order_idcustomercityitemstotal
1001Анна ПетроваМоскваНоутбук; Мышь78000
1002Иван СоколовСПбКлавиатура3500
1003Анна ПетроваМоскваМонитор; Мышь; Коврик32000

Здесь уже видны все будущие проблемы: в items лежит список через точку с запятой, город Анны записан дважды, а если Анна переедет — придётся править две строки. Разберём это по шагам.

Какие аномалии возникают в ненормализованной таблице?

Аномалия — это ситуация, когда простая операция с данными приводит к неверному или невозможному состоянию БД. Их ровно три типа, и именно от них защищает нормализация.

Аномалия вставки. Ты хочешь добавить в каталог новый товар «Веб-камера», который ещё никто не заказывал. Но структура таблицы orders_flat требует order_id и покупателя. Товар без заказа некуда положить — придётся выдумывать пустой заказ или оставлять NULL-строку. Факт о товаре не может существовать сам по себе.

Аномалия обновления. Анна Петрова переехала из Москвы в Казань. Её город записан в строках 1001 и 1003. Ты обновил одну строку и забыл про вторую — и теперь в базе Анна одновременно в двух городах. Любой отчёт по географии врёт. Чем больше заказов у клиента, тем выше шанс рассинхрона.

Аномалия удаления. Иван Соколов сделал единственный заказ 1002, а потом попросил его отменить. Ты удаляешь строку — и вместе с заказом исчезает вся информация об Иване: его имя, город. Данные о клиенте были привязаны только к заказу и умерли вместе с ним.

Все три аномалии — симптомы одного заболевания: данные, которые логически независимы, физически сцеплены в одной строке. Лечение — разнести их по отдельным таблицам. Кстати, похожая логика «один факт — одно место» лежит и в основе дедупликации: смотри разбор гэпов и дублей в SQL.

Что требует первая нормальная форма (1НФ)?

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

В нашей таблице поле items нарушает 1НФ грубее всего: там список товаров через разделитель. С таким полем нельзя посчитать, сколько раз заказывали мышь, нельзя сделать JOIN с каталогом, нельзя посчитать выручку по товару. Любая аналитика упирается в парсинг строки.

Приводим к 1НФ — разбиваем список так, чтобы одна строка описывала одну позицию заказа:

CREATE TABLE order_items_1nf (
    order_id   INT,
    customer   TEXT,
    city       TEXT,
    product    TEXT,
    qty        INT,
    price      NUMERIC(10,2),
    PRIMARY KEY (order_id, product)
);

INSERT INTO order_items_1nf VALUES
    (1001, 'Анна Петрова', 'Москва', 'Ноутбук',   1, 75000),
    (1001, 'Анна Петрова', 'Москва', 'Мышь',      1,  3000),
    (1002, 'Иван Соколов', 'СПб',    'Клавиатура', 1,  3500),
    (1003, 'Анна Петрова', 'Москва', 'Монитор',   1, 25000),
    (1003, 'Анна Петрова', 'Москва', 'Мышь',      2,  3000),
    (1003, 'Анна Петрова', 'Москва', 'Коврик',    1,  1000);

Теперь каждое значение атомарно, есть составной первичный ключ (order_id, product), и уже можно нормально агрегировать:

SELECT product, SUM(qty) AS total_sold, SUM(qty * price) AS revenue
FROM order_items_1nf
GROUP BY product
ORDER BY revenue DESC;

Это уже работающий SQL. Если хочешь потренировать GROUP BY на таких таблицах, начни с основ агрегатов в SQL с нуля, а разницу между GROUP BY и HAVING разбирает отдельный гайд.

Но 1НФ породила новую боль: город Анны теперь дублируется не в двух строках, а в четырёх. Аномалия обновления стала только острее. Это нормально — каждая форма чинит своё, поэтому идём дальше.

Что убирает вторая нормальная форма (2НФ)?

Вторая нормальная форма требует, чтобы таблица была в 1НФ и чтобы каждый неключевой атрибут зависел от всего первичного ключа, а не от его части. Частичные зависимости — вот что убирает 2НФ.

У нас составной ключ (order_id, product). Разберём, от чего зависит каждое поле:

Частичные зависимости — источник дублей. Раз city зависит только от order_id, он повторяется в каждой позиции заказа. Раз price зависит только от product, цена дублируется в каждом заказе с этим товаром.

Разносим по таблицам так, чтобы каждый атрибут жил рядом со своим ключом:

CREATE TABLE orders (
    order_id  INT PRIMARY KEY,
    customer  TEXT,
    city      TEXT
);

CREATE TABLE products (
    product   TEXT PRIMARY KEY,
    price     NUMERIC(10,2)
);

CREATE TABLE order_items (
    order_id  INT REFERENCES orders(order_id),
    product   TEXT REFERENCES products(product),
    qty       INT,
    PRIMARY KEY (order_id, product)
);

Теперь цена товара хранится один раз в products. Данные заказа — один раз в orders. А order_items держит только то, что зависит от полного ключа — количество. Аномалия вставки товара исчезла: новую «Веб-камеру» можно добавить в products, даже если её никто не заказывал. Собрать всё обратно легко через JOIN:

SELECT o.order_id, o.customer, oi.product, oi.qty, p.price,
       oi.qty * p.price AS line_total
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p     ON p.product   = oi.product;

Если JOIN пока пугает, разбери все типы соединений на примерах — без этого нормализованная схема остаётся набором разрозненных таблиц.

Что исправляет третья нормальная форма (3НФ)?

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

Усложним пример. Допустим, у товаров появилась категория, а у категории — ставка НДС:

-- нарушение 3НФ: vat_rate зависит от category, а не напрямую от product
CREATE TABLE products_bad (
    product    TEXT PRIMARY KEY,
    price      NUMERIC(10,2),
    category   TEXT,
    vat_rate   NUMERIC(4,2)
);

Здесь цепочка: product определяет category, а category определяет vat_rate. Ставка НДС зависит от товара транзитивно, через категорию. Результат — знакомая беда: ставка НДС дублируется в каждом товаре одной категории. Меняется ставка для «Электроники» — надо обновить десятки строк, и снова риск рассинхрона.

Убираем транзитивную зависимость — выносим категорию в свою таблицу:

CREATE TABLE categories (
    category   TEXT PRIMARY KEY,
    vat_rate   NUMERIC(4,2)
);

CREATE TABLE products (
    product    TEXT PRIMARY KEY,
    price      NUMERIC(10,2),
    category   TEXT REFERENCES categories(category)
);

Теперь ставка НДС для категории лежит ровно в одном месте. Обновляешь одну строку в categories — и она корректна для всех товаров этой категории. Это и есть 3НФ: каждый неключевой атрибут зависит от ключа, всего ключа и ничего кроме ключа.

Итоговое сравнение всех трёх форм:

ФормаЧто запрещаетЧто выносимАномалия, которую чинит
1НФСписки в ячейке, нет ключаКаждую позицию в свою строкуНевозможность агрегировать
2НФЗависимость от части ключаАтрибуты клиента и товараДубли и аномалия вставки
3НФЗависимость поля от поляКатегорию с её атрибутамиАномалия обновления

Для большинства продуктовых БД именно 3НФ — практический предел. Есть формы строже (Бойса-Кодда, 4НФ, 5НФ), но на собеседовании и в реальной работе аналитику достаточно уверенно объяснять первые три. Проверить, как их спрашивают, можно в банке из 150 вопросов по SQL.

Когда и зачем данные денормализуют под аналитику?

Денормализация — это осознанное нарушение нормальных форм ради скорости чтения. Звучит как ересь после всего написанного выше, но у нормализации и аналитики разные приоритеты, и это ключ к пониманию.

Нормализованная схема оптимальна для OLTP — систем, где много мелких записей и обновлений: оформить заказ, поменять адрес, отменить платёж. Здесь важна целостность, и дубли недопустимы. Разницу OLTP и OLAP подробно разбирает отдельная статья.

Аналитика — это OLAP: редкие записи, но тяжёлые чтения по миллионам строк с агрегацией. И здесь нормализация мешает. Чтобы построить отчёт «выручка по категориям и городам за месяц», приходится джойнить пять таблиц. Пять JOIN на 300 миллионах строк — это медленно и дорого.

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

Пример денормализованной витрины под отчётность:

CREATE TABLE sales_mart AS
SELECT
    oi.order_id,
    o.customer,
    o.city,
    oi.product,
    p.category,
    oi.qty,
    p.price,
    oi.qty * p.price AS revenue
FROM order_items oi
JOIN orders o     ON o.order_id = oi.order_id
JOIN products p   ON p.product  = oi.product;

Теперь отчёт по категориям и городам — это один проход без JOIN:

SELECT city, category, SUM(revenue) AS revenue
FROM sales_mart
GROUP BY city, category
ORDER BY revenue DESC;

Ключевой момент: денормализуют не «потому что лень нормализовать», а осознанно, приняв на себя ответственность за консистентность данных. Витрина sales_mart пересобирается по расписанию из нормализованных источников — обычно через инструменты вроде dbt или оркестратор Airflow. Разницу подходов к загрузке смотри в разборе ETL против ELT. А когда витрина считает продуктовые показатели вроде DAU или retention D7, денормализация экономит часы работы дашборда.

Как нормализация связана с JOIN и схемой БД?

Нормализация и JOIN — две стороны одной медали. Нормализация разбивает данные на таблицы, а JOIN собирает их обратно в момент запроса. Чем сильнее нормализована БД, тем больше JOIN нужно для отчёта — и тем важнее понимать схему.

Схема БД (ER-диаграмма) — это карта того, как таблицы связаны внешними ключами. Когда ты видишь order_items.order_id REFERENCES orders(order_id), ты знаешь, что позиции соединяются с заказами по order_id. Без понимания схемы аналитик пишет запросы наугад и получает раздутые результаты из-за неверных связей — классическая ошибка, когда JOIN по неполному ключу множит строки.

Практический навык здесь — читать связи и правильно выбирать тип соединения. Например, если нужны все клиенты, включая тех, у кого нет заказов, — это LEFT JOIN, а не INNER. Такие вещи проще один раз прорешать, чем прочитать. Собрать нормализованную схему обратно в отчёт помогает питон тоже — в pandas это merge, разбор в части про объединение датафреймов.

Быстрый чек-лист, как оценить схему на нормализованность:

Если ответ «да» — либо это OLTP-таблица, которую стоит нормализовать, либо аналитическая витрина, где дубли допустимы осознанно. Отличать одно от другого — и есть зрелость проектировщика.

Теория укладывается в голове только через практику. Собери нормализованную схему из трёх таблиц и напиши по ней JOIN руками в SQL-тренажёре — первые 5 задач бесплатно, дальше открывается Pro. Проверить те же навыки в питоне можно в Python-тренажёре, а прогнать себя в формате собеседования — на банке вопросов или в AI-мок-собесе, где интервьюер задаёт вопросы по схеме и разбирает ответы.

Нормализация — не самоцель и не academic-ритуал. Это инструмент, который защищает данные от рассинхрона в OLTP и который ты сознательно откладываешь в сторону, когда строишь быстрые витрины под аналитику. Понимаешь три формы и умеешь объяснить, почему в конкретном случае выбрал 3НФ или денормализацию, — значит, проектируешь схемы, а не просто пишешь CREATE TABLE.

Потренируй JOIN и агрегаты на нормализованных таблицах
Нормализация начинает иметь смысл, когда ты сам собираешь данные обратно через JOIN. В тренажёре 425 SQL-задач на реальных схемах: первые 5 бесплатно, дальше Pro.
Начать курс SQL →