Логотип Workflow

Article

Updated at:

Normalization

Stage 7. Нормализация: как не сломать данные в будущем

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

Normalization process

Представьте интернет-магазин, который хранит заказы. Первая соблазнительная версия — одна большая таблица, где вместе лежат данные заказа, клиента и товара. Сначала это удобно: одна строка будто содержит все, что нужно показать на экране заказа. Проблема появляется позже, когда один клиент делает много заказов, а один товар встречается в разных заказах. Имена, телефоны и данные товаров начинают повторяться. Как только повторяющееся значение меняется, база рискует получить несколько версий одной и той же информации.

ФормаПрактическая идеяЧто проверяем
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_idcustomer_nametotal_amount
101Anna2300.00
102Ivan900.00

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

Частые ошибки новичков:

  1. “Складывать все в одну таблицу, так проще”. Проще только в начале.
  2. Дублировать справочные данные, например телефон клиента или название товара, в десятках таблиц.
  3. Хранить списки через запятую вместо отдельных строк.
  4. Не фиксировать внешние ключи, из-за чего связи существуют только в коде приложения.
  5. Неправильно нормализовать исторические факты: например, считать старый заказ только через products.current_price.

Полезный компромисс: в OLTP-системах, где пользователи создают заказы, платежи, заявки и профили, исходная модель обычно должна быть нормализована. В аналитике иногда делают денормализацию для скорости чтения. Денормализация не плоха сама по себе, но это должна быть осознанная оптимизация, а не стартовая модель для основных транзакционных данных.

Короткий чеклист перед релизом схемы:

  • где хранится каждый бизнес-факт;
  • можно ли обновить один факт в одном месте;
  • является ли повторение данных историческим снимком или случайным дублем;
  • есть ли внешний ключ для связи;
  • можно ли объяснить, почему каждая таблица существует.

Если ответы понятны, у вас хорошая модель. Нормализация не усложняет проект, а снижает будущую стоимость изменений.

Практика перед следующим уроком

Попробуйте руками повторить примеры из статьи на маленьком наборе данных из 5-10 строк. Это важный шаг: когда вы сами запускаете SQL и видите конкретный результат таблицей, материал перестает быть абстрактным. Сначала выполните запрос без оптимизаций, затем внесите одно изменение и посмотрите, как меняется результат или план выполнения. Если что-то не сходится, зафиксируйте вопрос и проверьте: корректны ли фильтры, правильно ли выбраны поля, не потерялись ли строки из-за условий соединения, не появился ли NULL там, где вы его не ожидали.

Мини-проверка понимания:

  1. Могу объяснить тему урока одним простым предложением.
  2. Могу написать базовый SQL-пример без подсказки.
  3. Могу прочитать результат запроса и объяснить каждую колонку.
  4. Могу назвать минимум одну частую ошибку и как ее избежать.

Дополнительный сценарий: изменение номера телефона

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

UPDATE customers
SET phone = '+380501112233'
WHERE id = 42;

После этого любой JOIN с заказами покажет новый телефон без массовых обновлений. Это наглядный признак устойчивой модели данных: типовое изменение затрагивает нужную таблицу, а не всю базу.