Нормализация: 1НФ-3НФ без боли
Нормализация: 1НФ-3НФ без боли
Нормализация - это процесс организации данных так, чтобы:
- Не было дубликатов
- Каждое значение было на своём месте
- Запросы и обновления были легче
Звучит скучно, но в реальной жизни это означает: нет больше адских колонок типа phones = '111,222,333'.
Проблема: неправильная структура
Неправильно:
users( id, name, phones )
1 | Alice | 111,222,333
2 | Bob | 444,555
Проблемы:
- Как найти все номера Алисы?
WHERE phones LIKE '%111%'- медленно и хрупко - Как обновить один номер? Нужно парсить строку в приложении
- Как удалить номер? Нужна сложная логика со строками
- Легко сломать формат:
111;222;333(точка с запятой вместо запятой)
1НФ: First Normal Form - одно значение в ячейке
1НФ правило: каждая ячейка содержит только одно значение, не список.
Правильно:
users( id, name )
1 | Alice
2 | Bob
user_phones( id, user_id, phone )
1 | 1 | 111
2 | 1 | 222
3 | 1 | 333
4 | 2 | 444
5 | 2 | 555
Теперь:
- Легко найти все номера:
SELECT phone FROM user_phones WHERE user_id = 1 - Легко обновить:
UPDATE user_phones SET phone = '999' WHERE id = 1 - Легко удалить:
DELETE FROM user_phones WHERE id = 1
Цена за это - лишняя таблица: чтобы показать пользователя вместе с номерами, придётся сделать JOIN.
2НФ: Second Normal Form - зависит от ПОЛНОГО ключа
2НФ правило: каждое поле (не-ключевое) должно зависеть от полного PRIMARY KEY, не от его части.
Пример нарушения 2НФ (составной ключ):
CREATE TABLE order_items (
order_id BIGINT,
product_id BIGINT,
product_name TEXT, - зависит только от product_id, а не от (order_id, product_id)!
quantity INT, - зависит от пары (order_id, product_id)
PRIMARY KEY (order_id, product_id)
);
Проблема: product_name зависит только от product_id. Если один товар есть в 100 заказах, мы повторяем название 100 раз!
Правильно:
CREATE TABLE products (
product_id BIGSERIAL PRIMARY KEY,
product_name TEXT NOT NULL
);
CREATE TABLE order_items (
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
CONSTRAINT order_items_pk PRIMARY KEY (order_id, product_id),
CONSTRAINT order_items_product_fk FOREIGN KEY (product_id) REFERENCES products(product_id)
);
Теперь product_name хранится в одном месте.
3НФ: Third Normal Form - никаких транзитивных зависимостей
3НФ правило: нет зависимостей между не-ключевыми полями. Если колонка A определяет колонку B, а B определяет C, то это транзитивная зависимость - нарушение 3НФ.
Пример нарушения:
CREATE TABLE orders (
order_id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
city_id BIGINT NOT NULL,
city_name TEXT NOT NULL, - зависит от city_id, не от order_id!
country TEXT NOT NULL - зависит от city_id через city_name!
);
Если есть 1000 заказов из Москвы, мы 1000 раз пишем «Москва» и «Россия».
Правильно:
CREATE TABLE cities (
city_id BIGSERIAL PRIMARY KEY,
city_name TEXT NOT NULL,
country TEXT NOT NULL
);
CREATE TABLE orders (
order_id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
city_id BIGINT NOT NULL,
CONSTRAINT orders_city_fk FOREIGN KEY (city_id) REFERENCES cities(city_id)
);
Теперь данные о городе хранятся в одном месте.
Реальный пример: e-commerce
Неправильно (кашель и кровь):
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name TEXT,
price DECIMAL,
category_name TEXT, - дубликат категории, может быть typo
category_description TEXT, - дублируется, если категория с разными товарами
supplier_company TEXT, - дублируется много раз
supplier_contact_person TEXT,
supplier_email TEXT,
supplier_phone TEXT
);
Проблемы:
- Один товар из категории «Электроника» - значение повторяется 500 раз
- Обновить описание категории - нужно UPDATE 500 строк
- Опечатка: одна строка пишет «Elektronika», другая «Электроника»
- Один поставщик может иметь 1000 товаров - адрес повторяется 1000 раз
Правильно (3НФ):
CREATE TABLE categories (
category_id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT
);
CREATE TABLE suppliers (
supplier_id BIGSERIAL PRIMARY KEY,
company TEXT NOT NULL,
contact_person TEXT,
email TEXT,
phone TEXT
);
CREATE TABLE products (
product_id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
price DECIMAL NOT NULL,
category_id BIGINT NOT NULL,
supplier_id BIGINT NOT NULL,
CONSTRAINT products_category_fk FOREIGN KEY (category_id) REFERENCES categories(category_id),
CONSTRAINT products_supplier_fk FOREIGN KEY (supplier_id) REFERENCES suppliers(supplier_id)
);
Теперь:
- Каждая категория хранится один раз
- Каждый поставщик хранится один раз
- UPDATE категории касается одной строки
- Нет дубликатов, нет typo
Когда нарушать нормализацию (денормализация)
Иногда нормализация замедляет запросы. Тогда денормализируют осознанно - но сначала стоит убедиться через EXPLAIN, что тормозят действительно JOIN, а не отсутствующий индекс:
-- Медленно (много JOIN):
SELECT p.id, p.name, c.category_name, s.company
FROM products p
JOIN categories c ON p.category_id = c.id
JOIN suppliers s ON p.supplier_id = s.id
WHERE p.id = 123;
-- Быстро (denormalized):
CREATE TABLE products_denormalized (
product_id BIGSERIAL PRIMARY KEY,
name TEXT,
category_name TEXT, - копия из categories
supplier_company TEXT - копия из suppliers
);
SELECT * FROM products_denormalized WHERE product_id = 123;
Но тогда нужна логика для обновления всех копий (очень трудно). Используй только если:
- Запросы критически медленные
- Данные обновляются редко
- Есть materialized views или background jobs для обновления
Materialized Views для 3НФ + скорость
CREATE MATERIALIZED VIEW products_with_details AS
SELECT p.product_id, p.name, c.category_name, s.company
FROM products p
JOIN categories c ON p.category_id = c.id
JOIN suppliers s ON p.supplier_id = s.id;
-- Запрос очень быстро (данные предварительно вычислены)
SELECT * FROM products_with_details WHERE product_id = 123;
-- Обновляем view когда данные меняются
REFRESH MATERIALIZED VIEW products_with_details;
Видов (views) хранят запрос, materialized views хранят результаты. По сути это кеш внутри базы - с теми же вопросами, что и у внешнего кеша: когда обновлять и насколько устаревшие данные ты готов показывать.
Практические задания
- Дизайн школы: есть студенты, каждый в классе, каждый класс имеет учителя. Нарисуй 3НФ схему (3-4 таблицы)
- Возьми неправильную таблицу
books(id, title, author_name, author_country, author_birth_year)и нормализуй в 3НФ - Создай запрос к 3НФ схеме с двумя JOIN (книга + автор)
- Обсуди: когда денормализация оправдана? (e.g., поле
total_items_countв заказе вместо COUNT) - Проектируй блог: посты, комментарии, авторы, категории (минимум 5 таблиц в 3НФ)