Почему нормализация — это не про красоту схемы
Нормализация базы данных начинается не с теории, а с боли. Представьте таблицу заказов, где для каждого заказа указаны имя клиента, его адрес, название товара, категория и цена. Всё работает, пока не наступает момент обновления:
- Клиент сменил адрес. Теперь нужно найти все его заказы и обновить адрес в каждом.
- Товар переименовали. Новое название появляется только в новых заказах, а в старых остаётся старое.
- Категория товара изменилась. Придётся вручную искать все заказы с этим товаром и обновлять категорию.
Проблема не в том, что данные не обновляются — а в том, что их невозможно обновить согласованно. Нормализация решает эту проблему: она разбивает данные на таблицы так, чтобы каждый факт хранился ровно в одном месте. Адрес клиента — в таблице клиентов, название товара — в таблице товаров, категория — в таблице категорий. Тогда обновление адреса коснётся только одной строки, а не сотен заказов.
Но нормализация — это не волшебная таблетка. Она требует больше таблиц, больше соединений в запросах и сложнее в поддержке. Вопрос не в том, нормализовать или нет, а в том, до какой степени это делать и когда отступать.
Первые три нормальные формы без учебника
Первая нормальная форма: одно значение в ячейке
Первая нормальная форма требует, чтобы в каждой ячейке таблицы было ровно одно значение. Нельзя хранить список товаров в одной ячейке через запятую — их нужно вынести в отдельную таблицу.
Плохо:
Заказы:
| id | клиент | товары |
|----|---------|----------------------|
| 1 | Иванов | Хлеб, Молоко, Яйца |
Проблема здесь не только в том, что сложно посчитать количество каждого товара. Если товар “Хлеб” переименуют в “Хлеб бородинский”, придётся обновлять все строки, где он встречается. А если в одном заказе товары записаны через запятую, а в другом — через точку с запятой, поиск по ним станет невозможным.
Хорошо:
Заказы:
| id | клиент_id |
|----|-----------|
| 1 | 1 |
Заказанные товары:
| заказ_id | товар_id | количество |
|----------|----------|------------|
| 1 | 1 | 1 |
| 1 | 2 | 2 |
| 1 | 3 | 10 |
Теперь каждый товар в заказе — это отдельная строка. Можно легко посчитать количество каждого товара, обновить название в одном месте, и не придётся разбирать строку с товарами.
Вторая нормальная форма: зависимость от всего ключа
Вторая нормальная форма требует, чтобы все неключевые поля зависели от всего ключа, а не от его части. Если в таблице заказанных товаров есть поле “название товара”, оно зависит только от товар_id, а не от всего ключа (заказ_id, товар_id). Такие поля нужно вынести в отдельную таблицу.
Плохо:
Заказанные товары:
| заказ_id | товар_id | название товара | количество |
|----------|----------|-----------------|------------|
| 1 | 1 | Хлеб | 1 |
Проблема здесь в том, что название товара дублируется в каждом заказе. Если товар переименуют, придётся обновлять все строки, где он встречается. А если в разных заказах одно и то же название написано по-разному (“Хлеб” и “хлеб”), данные станут несогласованными.
Хорошо:
Заказанные товары:
| заказ_id | товар_id | количество |
|----------|----------|------------|
| 1 | 1 | 1 |
Товары:
| id | название |
|----|----------|
| 1 | Хлеб |
Теперь название товара хранится в одном месте, и его можно обновить без риска несогласованности.
Третья нормальная форма: независимость неключевых полей
Третья нормальная форма требует, чтобы неключевые поля не зависели друг от друга. Если в таблице клиентов есть поля “город” и “страна”, а страна однозначно определяется городом, то страну нужно вынести в отдельную таблицу.
Плохо:
Клиенты:
| id | имя | город | страна |
|----|-------|-----------|-----------|
| 1 | Иванов| Москва | Россия |
Проблема здесь в том, что если страна для города изменится (например, если город перейдёт под юрисдикцию другой страны), придётся обновлять все записи клиентов из этого города. А если в разных записях страна указана по-разному (“Россия” и “РФ”), данные станут несогласованными.
Хорошо:
Клиенты:
| id | имя | город_id |
|----|-------|----------|
| 1 | Иванов| 1 |
Города:
| id | название | страна_id |
|----|----------|-----------|
| 1 | Москва | 1 |
Страны:
| id | название |
|----|----------|
| 1 | Россия |
Теперь при изменении страны для города не придётся обновлять все записи клиентов. Страна хранится в одном месте, и её можно обновить без риска несогласованности.
Чем платят за нормализацию
Нормализация требует больше таблиц и больше соединений в запросах. Вместо одной таблицы заказов теперь нужно соединять таблицы заказов, клиентов, товаров и категорий. Запросы становятся длиннее, и их сложнее писать.
Пример запроса для получения списка заказов с именами клиентов и названиями товаров:
SELECT
заказы.id,
клиенты.имя,
товары.название,
заказанные_товары.количество
FROM заказы
JOIN клиенты ON заказы.клиент_id = клиенты.id
JOIN заказанные_товары ON заказы.id = заказанные_товары.заказ_id
JOIN товары ON заказанные_товары.товар_id = товары.id;
Без нормализации запрос был бы проще, но данные быстро стали бы несогласованными. Нормализация — это компромисс между согласованностью данных и сложностью запросов.
Тупик: начинают с нормализации до третьей формы
Первое, что пробуют инженеры, столкнувшись с несогласованностью данных — нормализовать схему до третьей нормальной формы. Это кажется логичным: если первая форма решает проблему с атомарными значениями, вторая — с зависимостями от части ключа, а третья — с зависимостями между неключевыми полями, то почему бы не сделать всё сразу?
Проблема в том, что нормализация до третьей формы не всегда решает реальные проблемы, а иногда даже усугубляет их. Представьте, что у вас есть таблица заказов, где для каждого заказа указаны клиент, товар и категория товара. Вы нормализуете её до третьей формы: выносите клиентов в отдельную таблицу, товары — в другую, категории — в третью.
Теперь, чтобы получить список заказов с именами клиентов и категориями товаров, нужно соединить четыре таблицы. Если таблица заказов большая, такой запрос может работать медленно. А если вам нужно часто получать количество заказов по категориям, придётся каждый раз соединять таблицы заказов, товаров и категорий.
Нормализация до третьей формы — это не панацея. Она решает проблему несогласованности данных, но создаёт проблему с производительностью запросов. Прежде чем нормализовать схему, нужно понять, какие проблемы вы решаете и какие новые проблемы создаёте.
Когда отступать от нормализации
Нормализация — это не догма. Иногда дублирование данных оправдано. Например, в витринах данных, отчётах или счётчиках. Если нужно часто получать количество заказов по клиентам, можно хранить это число в таблице клиентов и обновлять его при каждом изменении заказов.
Пример:
Клиенты:
| id | имя | количество_заказов |
|----|-------|--------------------|
| 1 | Иванов| 5 |
Но за это придётся платить: при каждом изменении заказов нужно обновлять счётчик в таблице клиентов. Если этого не сделать, данные станут несогласованными. Дублирование данных — это компромисс между производительностью и согласованностью.
Отступление от нормализации оправдано, если есть измерение, доказывающее необходимость. Например, если запросы к нормализованной схеме работают слишком медленно, а денормализованная версия ускоряет их в десятки раз. Если таких измерений нет, отступление — это преждевременная оптимизация.
Признак, по которому видно, что отступили рано — отсутствие измерений. Если вы денормализовали схему, потому что “так быстрее”, но не измерили, насколько быстрее, и не сравнили с альтернативами (например, с индексами), то вы отступили рано. Денормализация без измерений — это гадание на кофейной гуще.
Позиция редакции
Нормализация нужна, чтобы данные оставались согласованными. Но она не должна становиться самоцелью. Если дублирование данных ускоряет запросы и есть измерения, подтверждающие это, отступайте от нормализации. Главное — понимать, чем вы платите за каждое решение, и не отступать раньше времени.
Нормализация неверна, если:
- данные никогда не обновляются (например, логи или архивные записи);
- дублирование не приводит к несогласованности (например, неизменяемые справочники);
- производительность важнее согласованности (например, в аналитических системах).
Но даже в этих случаях нормализация может быть полезна. Например, в аналитических системах данные часто обновляются редко, но их объём огромен. Нормализация может помочь сэкономить место на диске и ускорить загрузку данных. Всё зависит от конкретной задачи и конкретных данных.