Нормализация
Нормализация¶
Нормализация — процесс организации данных в реляционной БД для уменьшения избыточности и предотвращения аномалий при вставке, обновлении и удалении.
Зачем нормализовать¶
Без нормализации данные дублируются, а дубликаты рано или поздно расходятся. Три классические аномалии:
| Аномалия | Пример |
|---|---|
| Вставки | Нельзя добавить нового клиента, пока он не сделал заказ |
| Обновления | Изменил адрес клиента в одной строке, забыл в другой — данные противоречат |
| Удаления | Удалил последний заказ клиента — потерял информацию о клиенте |
Нормализация устраняет эти аномалии, разбивая данные на связанные таблицы.
Ключевые понятия¶
Прежде чем разбирать формы, уточним терминологию:
| Термин | Значение |
|---|---|
| Суперключ | Набор атрибутов, однозначно определяющий строку (может содержать лишние поля) |
| Потенциальный ключ | Минимальный суперключ — убери любой атрибут, и уникальность пропадёт |
| Первичный ключ | Один из потенциальных ключей, выбранный основным |
| Функциональная зависимость | A → B: зная значение A, ты однозначно определяешь B |
1NF — первая нормальная форма¶
Правило: каждая ячейка содержит одно атомарное значение. Нет повторяющихся групп, массивов или вложенных таблиц.
Что считать атомарным?
Понятие атомарности относительно. ФИО — для почтового сервиса это одна строка (атомарно), а для кадровой системы — три поля (фамилия, имя, отчество). PostgreSQL поддерживает JSONB и массивы, что позволяет осознанно нарушать 1NF ради гибкости — но ценой усложнения запросов и потери контроля целостности.
Пример: таблица products из etl_kitchen_db¶
Столбец attributes типа JSONB хранит сразу несколько характеристик товара — с точки зрения классической теории это нарушение 1NF:
| product_id | name | attributes |
|---|---|---|
| 1 | iPhone 15 | {"color": "black", "storage_gb": 128, "brand": "Apple"} |
| 4 | MacBook Air M3 | {"ram_gb": 16, "storage_gb": 512, "brand": "Apple"} |
Приведение к 1NF¶
Выносим атрибуты в отдельную таблицу, где каждому атрибуту соответствует одна строка:
CREATE TABLE product_attributes_1nf (
product_id INT REFERENCES products(product_id),
attr_name TEXT,
attr_value TEXT,
PRIMARY KEY (product_id, attr_name)
);
INSERT INTO product_attributes_1nf (product_id, attr_name, attr_value)
SELECT product_id, key, value
FROM products, jsonb_each_text(attributes);
| product_id | attr_name | attr_value |
|---|---|---|
| 1 | brand | Apple |
| 1 | color | black |
| 1 | storage_gb | 128 |
| 4 | brand | Apple |
| 4 | ram_gb | 16 |
| 4 | storage_gb | 512 |
Теперь ты можешь отфильтровать все товары Apple обычным WHERE attr_value = 'Apple' без JSONB-функций.
2NF — вторая нормальная форма¶
Правило: таблица в 1NF + каждый неключевой атрибут полностью зависит от всего составного ключа, а не от его части.
2NF актуальна только для таблиц с составным ключом. Если у тебя простой PK (один столбец) и таблица в 1NF — она автоматически в 2NF.
Пример: order_items с лишним полем¶
Представь, что в order_items добавили product_name для удобства. Ключ — {order_id, product_id}:
-- Антипаттерн: частичная зависимость
CREATE TABLE bad_order_items (
order_id INT REFERENCES orders(order_id),
product_id INT REFERENCES products(product_id),
product_name TEXT, -- зависит только от product_id!
quantity INT,
PRIMARY KEY (order_id, product_id)
);
| order_id | product_id | product_name | quantity |
|---|---|---|---|
| 1 | 1 | iPhone 15 | 1 |
| 18 | 1 | iPhone 15 | 1 |
| 27 | 1 | iPhone 15 | 1 |
product_name дублируется в каждой строке. Переименуешь товар — обновлять придётся везде. Забудешь одну строку — аномалия обновления.
Декомпозиция¶
-- order_items: только связи и данные, зависящие от всего ключа
CREATE TABLE order_items_fixed (
order_id INT REFERENCES orders(order_id),
product_id INT REFERENCES products(product_id),
quantity INT,
PRIMARY KEY (order_id, product_id)
);
-- products: описание товара в одном месте
CREATE TABLE products_fixed (
product_id INT PRIMARY KEY,
name TEXT
);
Название товара хранится ровно в одном месте — классический принцип «ничего, кроме ключа».
3NF — третья нормальная форма¶
Правило: таблица в 2NF + нет транзитивных зависимостей. Неключевой атрибут не должен зависеть от другого неключевого атрибута.
По формуле Уильяма Кента: поле содержит факт «о ключе, о всём ключе и ни о чём, кроме ключа».
Пример: customers из etl_kitchen_db¶
В таблице customers есть поля city и region. Если город однозначно определяет регион (Москва → Центральный), возникает цепочка:
customer_id → city— знаем город клиентаcity → region— город определяет регионcustomer_id → region— транзитивная зависимость через город
| customer_id | name | city | region |
|---|---|---|---|
| 1 | Алексей Смирнов | Москва | Центральный |
| 5 | Сергей Козлов | Москва | Центральный |
| 11 | Андрей Петров | Москва | Центральный |
Регион Центральный дублируется для каждого москвича.
Декомпозиция¶
-- Справочник городов
CREATE TABLE cities (
city_name TEXT PRIMARY KEY,
region_name TEXT
);
-- Клиенты ссылаются на город
CREATE TABLE customers_3nf (
customer_id INT PRIMARY KEY,
name TEXT,
city TEXT REFERENCES cities(city_name)
);
Теперь регион хранится в одном месте — в справочнике cities.
BCNF — форма Бойса-Кодда¶
Усиленная 3NF: каждый детерминант (левая часть функциональной зависимости) является суперключом. 3NF допускает исключения для ключевых атрибутов, BCNF — нет.
На практике 3NF = BCNF для большинства таблиц. Разница возникает при перекрывающихся составных ключах.
Классический пример: бронирование кортов¶
| court | start_time | rate_type |
|---|---|---|
| 1 | 09:00 | SAVER |
| 1 | 10:00 | STANDARD |
| 2 | 09:00 | PREMIUM-A |
Зависимости:
{court, start_time} → rate_type— пара определяет тарифrate_type → court— каждый тариф привязан к конкретному корту
Потенциальные ключи: {court, start_time} и {rate_type, start_time}. Все атрибуты входят в ключи — 3NF соблюдена. Но rate_type → court, а rate_type сам по себе не суперключ — BCNF нарушена.
Исправление: разбить на rate_types(rate_type, court) и bookings(rate_type, start_time).
Когда это важно
На собеседовании BCNF спрашивают часто, но на практике большинство таблиц с простым PK автоматически в BCNF. Помни про этот кейс для таблиц с несколькими составными ключами.
Денормализация¶
Денормализация — осознанное добавление избыточности для ускорения чтения. Ты жертвуешь производительностью записи, чтобы избежать тяжёлых JOIN в аналитических запросах.
| Нормализация | Денормализация |
|---|---|
| Меньше избыточности | Быстрее чтение (меньше JOIN) |
| Целостность данных | Избыточность данных |
| Больше JOIN | Сложнее обновление |
| OLTP-системы | OLAP / DWH |
Пример: витрина продаж по категориям¶
CREATE MATERIALIZED VIEW category_sales_mart AS
SELECT
c.name AS category_name,
SUM(oi.quantity) AS total_quantity,
SUM(oi.quantity * oi.unit_price) AS total_revenue,
COUNT(DISTINCT oi.order_id) AS orders_count
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
JOIN categories c ON p.category_id = c.category_id
GROUP BY c.name;
-- Обновление после загрузки новых данных:
REFRESH MATERIALIZED VIEW category_sales_mart;
| category_name | total_quantity | total_revenue | orders_count |
|---|---|---|---|
| Смартфоны | 42 | 3 219 580.00 | 28 |
| Ноутбуки | 15 | 1 424 850.00 | 12 |
| Аксессуары | 89 | 1 156 220.00 | 45 |
Антипаттерн: денормализация в OLTP¶
Хранить customer_name прямо в таблице orders при 1000 записей в секунду — плохая идея. Каждое обновление имени клиента требует обновления всех его заказов, а рассинхронизация данных — вопрос времени.
Правило для DE
Нормализуй на входе (staging), денормализуй на выходе (mart). В ODS/raw — храни как есть. В DWH — моделируй по Star/Vault. В mart — плоские таблицы для аналитиков.
Нормализация vs модели DWH¶
| Подход | Где применять | Примеры |
|---|---|---|
| 3NF | OLTP-системы, staging-слой | Операционные БД |
| Star Schema | DWH, аналитический слой | Факты + измерения |
| Snowflake Schema | DWH с нормализованными измерениями | Иерархии в измерениях |
| Data Vault | Слой интеграции (raw vault) | Hubs + Links + Satellites |
Как определить нормальную форму¶
- Есть повторяющиеся группы, массивы или JSON в ячейках? → Не 1NF
- Есть составной ключ и часть атрибутов зависит только от части ключа? → Не 2NF
- Есть атрибут, который зависит от другого неключевого атрибута? → Не 3NF
- Есть детерминант, который не является суперключом? → Не BCNF
Что запомнить¶
- Нормализация защищает от аномалий вставки, обновления и удаления
- 1NF — атомарные значения, нет повторяющихся групп (массивов, JSON)
- 2NF — все поля зависят от всего составного ключа, а не от его части
- 3NF — неключевые поля не зависят друг от друга (нет транзитивных зависимостей)
- BCNF — каждый детерминант является суперключом (актуально при перекрывающихся составных ключах)
- Для OLTP — нормализуй до 3NF, для DWH — денормализуй в Star Schema / витрины
- Главное правило DE: нормализуй на входе (staging), денормализуй на выходе (mart)
Попробуй сам¶
Интерактивная песочница с настоящим PostgreSQL прямо в браузере. База etl_kitchen_db уже загружена — попробуй развернуть JSONB в плоскую таблицу или найти транзитивные зависимости.
Проверь себя¶
Источники¶
- Database normalization — Wikipedia — формальные определения нормальных форм (1NF–BCNF)
- PostgreSQL: Constraints — ограничения целостности и внешние ключи
- Kimball Group: Dimensional Modeling Techniques — денормализация в контексте DWH