Stage 7. Нормализация: как не сломать данные в будущем
Нормализация — это способ разложить данные по таблицам так, чтобы одна и та же бизнес-информация не копировалась в десятки мест. Смысл не в том, чтобы сделать схему “красивой по учебнику”. Смысл практический: когда клиент меняет телефон, у товара исправляют название или в заказ добавляют позицию, в базе должно быть понятно, где именно поддерживается эта информация.
Представьте интернет-магазин, который хранит заказы. Первая соблазнительная версия — одна большая таблица, где вместе лежат данные заказа, клиента и товара. Сначала это удобно: одна строка будто содержит все, что нужно показать на экране заказа. Проблема появляется позже, когда один клиент делает много заказов, а один товар встречается в разных заказах. Имена, телефоны и данные товаров начинают повторяться. Как только повторяющееся значение меняется, база рискует получить несколько версий одной и той же информации.
| Форма | Практическая идея | Что проверяем |
|---|---|---|
| 1NF | В ячейке одно значение | Нет списков “через запятую” |
| 2NF | Неключевые поля описывают весь ключ | Поле не зависит только от части составного ключа |
| 3NF | Поля описывают строку, а не другое поле | Нет скрытого копирования данных из другой таблицы |
Пример плохой схемы
orders(id, order_date, customer_name, customer_phone, product_name, product_price)
Эта таблица смешивает несколько разных вещей: сам заказ, профиль клиента и каталог товаров. Из-за этого появляются реальные проблемы поддержки:
customer_phoneповторяется в каждой строке заказа;product_priceстановится неоднозначным, потому что текущая цена товара и цена в момент покупки — разные факты;- один заказ с несколькими товарами трудно представить без повторения полей заказа;
- обновления становятся рискованными: одна пропущенная строка уже создает противоречие.
Нормализованный вариант
customers(id, name, phone)
products(id, name, current_price)
orders(id, customer_id, order_date)
order_items(order_id, product_id, qty, price_at_purchase)
Теперь у данных есть понятные владельцы. Контакты клиента поддерживаются в customers. Каталог товаров — в products. Сам заказ как событие покупки — в orders. Состав заказа — в order_items. Цена на момент покупки хранится в order_items.price_at_purchase, потому что это часть истории заказа, а не текущая цена товара в каталоге.
Нормальные формы простыми словами
Первая нормальная форма означает, что в одной ячейке хранится одно значение, а не список. Например, product_names = 'Keyboard, Mouse, Cable' внутри одной строки заказа — плохой признак. Разные товары должны стать отдельными строками в order_items.
Вторая нормальная форма важна, когда у таблицы составной ключ, например (order_id, product_id). Каждое неключевое поле должно зависеть от всего ключа. В order_items поле qty зависит и от заказа, и от товара: оно описывает, сколько единиц этого товара купили именно в этом заказе. А вот customer_phone от пары (order_id, product_id) не зависит, значит, ему не место в этой таблице.
Третья нормальная форма говорит, что поле не должно зависеть от другого неключевого поля. Если в orders хранить и customer_id, и customer_phone, телефон фактически зависит от клиента, а не от самого заказа. Это скрытое дублирование. Заказ должен ссылаться на клиента, а телефон должен оставаться в customers.
Запрос после нормализации
SELECT o.id,
c.name,
SUM(oi.qty * oi.price_at_purchase) AS total_amount
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN order_items oi ON oi.order_id = o.id
GROUP BY o.id, c.name
ORDER BY o.id;
Результат:
| order_id | customer_name | total_amount |
|---|---|---|
| 101 | Anna | 2300.00 |
| 102 | Ivan | 900.00 |
Такой запрос — нормальное следствие нормализованной схемы. Данные разделены по нескольким таблицам, а JOIN собирает их в полный вид тогда, когда приложению нужен экран или отчет. Нормализация не запрещает показывать пользователю всю информацию вместе. Она запрещает постоянно хранить каждую деталь в нескольких местах только потому, что одному экрану удобно видеть все сразу.
Частые ошибки новичков:
- “Складывать все в одну таблицу, так проще”. Проще только в начале.
- Дублировать справочные данные, например телефон клиента или название товара, в десятках таблиц.
- Хранить списки через запятую вместо отдельных строк.
- Не фиксировать внешние ключи, из-за чего связи существуют только в коде приложения.
- Неправильно нормализовать исторические факты: например, считать старый заказ только через
products.current_price.
Полезный компромисс: в OLTP-системах, где пользователи создают заказы, платежи, заявки и профили, исходная модель обычно должна быть нормализована. В аналитике иногда делают денормализацию для скорости чтения. Денормализация не плоха сама по себе, но это должна быть осознанная оптимизация, а не стартовая модель для основных транзакционных данных.
Короткий чеклист перед релизом схемы:
- где хранится каждый бизнес-факт;
- можно ли обновить один факт в одном месте;
- является ли повторение данных историческим снимком или случайным дублем;
- есть ли внешний ключ для связи;
- можно ли объяснить, почему каждая таблица существует.
Если ответы понятны, у вас хорошая модель. Нормализация не усложняет проект, а снижает будущую стоимость изменений.
Практика перед следующим уроком
Попробуйте руками повторить примеры из статьи на маленьком наборе данных из 5-10 строк. Это важный шаг: когда вы сами запускаете SQL и видите конкретный результат таблицей, материал перестает быть абстрактным. Сначала выполните запрос без оптимизаций, затем внесите одно изменение и посмотрите, как меняется результат или план выполнения. Если что-то не сходится, зафиксируйте вопрос и проверьте: корректны ли фильтры, правильно ли выбраны поля, не потерялись ли строки из-за условий соединения, не появился ли NULL там, где вы его не ожидали.
Мини-проверка понимания:
- Могу объяснить тему урока одним простым предложением.
- Могу написать базовый SQL-пример без подсказки.
- Могу прочитать результат запроса и объяснить каждую колонку.
- Могу назвать минимум одну частую ошибку и как ее избежать.
Дополнительный сценарий: изменение номера телефона
Проверка нормализации на практике очень простая: что произойдет, если клиент сменит номер телефона? В правильной схеме вы меняете одну строку в customers. В плохой схеме номер скопирован в заказы, счета, доставки и обращения в поддержку, поэтому часть системы почти неизбежно оставит старое значение.
UPDATE customers
SET phone = '+380501112233'
WHERE id = 42;
После этого любой JOIN с заказами покажет новый телефон без массовых обновлений. Это наглядный признак устойчивой модели данных: типовое изменение затрагивает нужную таблицу, а не всю базу.