Нормальные формы: 1NF, 2NF, 3NF

Нормальные формы: 1NF, 2NF, 3NF

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

Аномалии ненормализованной схемы

Представьте таблицу «заказы» с данными о клиентах прямо в строках:

-- Денормализованная таблица (плохой дизайн)
CREATE TABLE orders_bad (
    order_id      INTEGER,
    customer_name TEXT,
    customer_city TEXT,
    product_name  TEXT,
    quantity      INTEGER
);
order_idcustomer_namecustomer_cityproduct_namequantity
1АннаМоскваНоутбук1
2АннаМоскваМышь2
3БорисПитерНоутбук1

Проблемы:

  • Аномалия обновления: Анна переехала? Нужно обновить каждую строку с её заказами
  • Аномалия вставки: нельзя добавить клиента без заказа
  • Аномалия удаления: удалили последний заказ Бориса — потеряли информацию о Борисе как клиенте

Нормализация устраняет эти аномалии, разбивая данные на отдельные таблицы с чёткими зависимостями.

1NF: первая нормальная форма

Условие: каждая ячейка содержит одно атомарное (неделимое) значение.

Нарушения 1NF:

  • Несколько значений в одной ячейке (список через запятую)
  • Повторяющиеся группы колонок
-- Нарушение 1NF: несколько телефонов в одной ячейке
INSERT INTO customers (name, phones) VALUES ('Анна', '123, 456, 789');

-- Нарушение 1NF: повторяющиеся группы
CREATE TABLE orders_bad2 (
    id        INTEGER,
    product1  TEXT,
    product2  TEXT,
    product3  TEXT    -- что если 4 товара?
);

Приведение к 1NF — разбить на атомарные значения:

-- Правильно: отдельная таблица для телефонов
CREATE TABLE customer_phones (
    customer_id INTEGER REFERENCES customers(id),
    phone       TEXT NOT NULL,
    PRIMARY KEY (customer_id, phone)
);

-- Правильно: отдельная таблица для позиций заказа
CREATE TABLE order_items (
    order_id   INTEGER REFERENCES orders(id),
    product_id INTEGER REFERENCES products(id),
    quantity   INTEGER NOT NULL,
    PRIMARY KEY (order_id, product_id)
);

Правило 1NF: одна строка = один факт. Нет множественных значений в ячейке, нет повторяющихся группочек колонок.

2NF: вторая нормальная форма

Условие: выполнена 1NF + каждый не-ключевой атрибут полностью зависит от первичного ключа.

Нарушение 2NF возникает при составном ключе, когда некоторые колонки зависят только от части ключа (частичная зависимость):

-- Составной PK: (order_id, product_id)
-- Нарушение 2NF: product_name зависит только от product_id, не от всего PK
CREATE TABLE order_items_bad (
    order_id     INTEGER,
    product_id   INTEGER,
    product_name TEXT,     -- частичная зависимость! зависит только от product_id
    quantity     INTEGER,
    PRIMARY KEY (order_id, product_id)
);
order_idproduct_idproduct_namequantity
110Ноутбук1
210Ноутбук1
311Мышь2

product_name повторяется для каждого product_id. При переименовании ноутбука — нужно обновить все строки с product_id = 10.

Приведение к 2NF — вынести product_name в отдельную таблицу:

CREATE TABLE products (
    id   SERIAL PRIMARY KEY,
    name TEXT NOT NULL
);

CREATE TABLE order_items (
    order_id   INTEGER REFERENCES orders(id),
    product_id INTEGER REFERENCES products(id),
    quantity   INTEGER NOT NULL,
    PRIMARY KEY (order_id, product_id)
    -- product_name убрали — оно в products.name
);

Правило 2NF: каждая не-ключевая колонка должна зависеть от всего первичного ключа, а не от его части.

3NF: третья нормальная форма

Условие: выполнена 2NF + нет транзитивных зависимостей (не-ключевой атрибут не зависит от другого не-ключевого атрибута).

-- Нарушение 3NF: zip_code → city (транзитивная зависимость)
CREATE TABLE customers_bad (
    id       SERIAL PRIMARY KEY,
    name     TEXT,
    zip_code VARCHAR(10),
    city     TEXT    -- зависит от zip_code, а не от id напрямую
);
idnamezip_codecity
1Анна101000Москва
2Борис101000Москва
3Вера191000Питер

city определяется через zip_code, а не напрямую через id. При изменении города для индекса 101000 нужно обновить много строк.

Приведение к 3NF:

CREATE TABLE zip_codes (
    zip_code VARCHAR(10) PRIMARY KEY,
    city     TEXT NOT NULL
);

CREATE TABLE customers (
    id       SERIAL PRIMARY KEY,
    name     TEXT,
    zip_code VARCHAR(10) REFERENCES zip_codes(zip_code)
);

Правило 3NF: не-ключевые атрибуты должны зависеть только от первичного ключа, а не от других не-ключевых атрибутов.

Сводная таблица нормальных форм

ФормаУсловиеЧто устраняет
1NFАтомарные значения, нет повторяющихся группМногозначные ячейки
2NF1NF + нет частичных зависимостейДублирование при составном ключе
3NF2NF + нет транзитивных зависимостейЦепочки зависимостей через не-ключ

На практике большинство хорошо спроектированных схем автоматически удовлетворяют 3NF, если следовать принципу «каждая таблица описывает одну сущность».

BCNF и 4NF: кратко

За 3NF существуют ещё более строгие нормальные формы. Для практической работы они нужны редко, но знать стоит.

BCNF (нормальная форма Бойса-Кодда): усиленная 3NF. Нарушается, когда детерминант функциональной зависимости — не потенциальный ключ. На практике большинство таблиц в 3NF автоматически в BCNF.

4NF: устраняет многозначные зависимости. Нарушается, когда одна колонка независимо связана с несколькими другими в одной таблице:

-- Нарушение 4NF: languages и frameworks независимы друг от друга
-- (developer говорит на Python И Python разработчик знает Django — независимые факты)
CREATE TABLE developer_skills_bad (
    developer_id  INTEGER,
    language      TEXT,    -- Python, JavaScript
    framework     TEXT     -- Django, React (не зависит от language)
);
-- Решение: две отдельные таблицы developer_languages и developer_frameworks

Для работы с реальными приложениями достаточно понимать и применять 1NF–3NF.

Нормализация vs денормализация

Нормализация устраняет аномалии, но иногда осознанная денормализация оправдана:

Когда денормализовать:

  • Аналитические запросы, требующие сотен JOIN — денормализованная таблица читается быстрее
  • price_snapshot в позиции заказа — цена на момент покупки не должна меняться при изменении текущей цены товара
  • Агрегаты (кэшированный order_count в профиле) — чтобы не считать каждый раз
-- Денормализация оправдана: цена фиксируется при заказе
CREATE TABLE order_items (
    order_id    INTEGER REFERENCES orders(id),
    product_id  INTEGER REFERENCES products(id),
    unit_price  NUMERIC(10, 2) NOT NULL,  -- снапшот цены, не ссылка
    quantity    INTEGER NOT NULL
);

Правило: нормализуйте сначала, денормализуйте осознанно — когда есть измеренная проблема производительности или бизнес-требование хранить исторические значения.

Практический пример: нормализация схемы магазина

До нормализации (одна таблица):

order_id | customer_name | customer_city | product_name | category | quantity | price

После 1NF (убираем многозначные ячейки, выделяем повторяющиеся группы):

  • orders(id, customer_id, created_at)
  • order_items(order_id, product_id, quantity, unit_price)

После 2NF (убираем частичные зависимости):

  • customers(id, name, city) — выделяем клиента
  • products(id, name, category_id, price) — выделяем товар

После 3NF (убираем транзитивные зависимости):

  • categories(id, name) — название категории зависит от category_id, не от product.id

Итог — нормализованная схема из 4 таблиц, которую мы использовали на протяжении всего курса.

Итеративный процесс нормализации

На практике нормализацию применяют итеративно, а не «в одном проходе»:

  1. Выписать все данные, которые нужно хранить
  2. Объединить в одну «плоскую» таблицу (ненормализованная форма)
  3. Привести к 1NF: убрать повторяющиеся группы и массивы
  4. Привести к 2NF: выделить сущности для составных ключей
  5. Привести к 3NF: убрать транзитивные зависимости
  6. Проверить: каждая таблица описывает одну сущность?

Быстрый тест для шага 6 — задайте вопрос: «Можно ли описать назначение этой таблицы одной фразой?» Если нет — скорее всего нарушена нормализация.

customers — хранит клиентов ✓
orders — хранит заказы ✓
order_items — хранит позиции заказов ✓
products — хранит товары ✓
orders_and_customers_and_products — ??? ✗ (нарушение нормализации)

Проверь себя

Таблица employees содержит колонки id, name, department_name, department_head. Нарушает ли это 3NF? Как привести к 3NF?

Краткий итог

  • 1NF: атомарные значения, нет повторяющихся групп — одна ячейка = один факт
  • 2NF: нет частичных зависимостей — не-ключи зависят от всего составного ключа
  • 3NF: нет транзитивных зависимостей — не-ключи зависят только от ключа
  • Нормализация устраняет аномалии обновления, вставки и удаления
  • Денормализация допустима осознанно: снапшоты цен, кэшированные счётчики, аналитика

Что дальше

Нормальные формы описывают правила. Как их визуализировать и донести до команды? Следующий урок — ER-диаграммы: способ изобразить схему и связи графически.

Попробуйте интерактивную версию

Практические задачи, квизы и AI-наставник — бесплатный старт без карты

Перейти к практике