Нормальные формы: 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_id | customer_name | customer_city | product_name | quantity |
|---|---|---|---|---|
| 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_id | product_id | product_name | quantity |
|---|---|---|---|
| 1 | 10 | Ноутбук | 1 |
| 2 | 10 | Ноутбук | 1 |
| 3 | 11 | Мышь | 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 напрямую
);
| id | name | zip_code | city |
|---|---|---|---|
| 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 | Атомарные значения, нет повторяющихся групп | Многозначные ячейки |
| 2NF | 1NF + нет частичных зависимостей | Дублирование при составном ключе |
| 3NF | 2NF + нет транзитивных зависимостей | Цепочки зависимостей через не-ключ |
На практике большинство хорошо спроектированных схем автоматически удовлетворяют 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 таблиц, которую мы использовали на протяжении всего курса.
Итеративный процесс нормализации
На практике нормализацию применяют итеративно, а не «в одном проходе»:
- Выписать все данные, которые нужно хранить
- Объединить в одну «плоскую» таблицу (ненормализованная форма)
- Привести к 1NF: убрать повторяющиеся группы и массивы
- Привести к 2NF: выделить сущности для составных ключей
- Привести к 3NF: убрать транзитивные зависимости
- Проверить: каждая таблица описывает одну сущность?
Быстрый тест для шага 6 — задайте вопрос: «Можно ли описать назначение этой таблицы одной фразой?» Если нет — скорее всего нарушена нормализация.
customers — хранит клиентов ✓
orders — хранит заказы ✓
order_items — хранит позиции заказов ✓
products — хранит товары ✓
orders_and_customers_and_products — ??? ✗ (нарушение нормализации)
Проверь себя
Таблица employees содержит колонки id, name, department_name, department_head. Нарушает ли это 3NF? Как привести к 3NF?
Краткий итог
- 1NF: атомарные значения, нет повторяющихся групп — одна ячейка = один факт
- 2NF: нет частичных зависимостей — не-ключи зависят от всего составного ключа
- 3NF: нет транзитивных зависимостей — не-ключи зависят только от ключа
- Нормализация устраняет аномалии обновления, вставки и удаления
- Денормализация допустима осознанно: снапшоты цен, кэшированные счётчики, аналитика
Что дальше
Нормальные формы описывают правила. Как их визуализировать и донести до команды? Следующий урок — ER-диаграммы: способ изобразить схему и связи графически.