Webrium
Подписаться

Нормализация: до какой степени и когда отступать

Как избежать дублирования данных и упростить обновление информации в базе с помощью нормализации

Схема разделения таблицы: слева дублирующиеся данные, справа нормализованные с единым источником обновленияДублирование данныхЕдиный источник

Почему нормализация — это не про красоту схемы

Нормализация базы данных начинается не с теории, а с боли. Представьте таблицу заказов, где для каждого заказа указаны имя клиента, его адрес, название товара, категория и цена. Всё работает, пока не наступает момент обновления:

  • Клиент сменил адрес. Теперь нужно найти все его заказы и обновить адрес в каждом.
  • Товар переименовали. Новое название появляется только в новых заказах, а в старых остаётся старое.
  • Категория товара изменилась. Придётся вручную искать все заказы с этим товаром и обновлять категорию.

Проблема не в том, что данные не обновляются — а в том, что их невозможно обновить согласованно. Нормализация решает эту проблему: она разбивает данные на таблицы так, чтобы каждый факт хранился ровно в одном месте. Адрес клиента — в таблице клиентов, название товара — в таблице товаров, категория — в таблице категорий. Тогда обновление адреса коснётся только одной строки, а не сотен заказов.

Но нормализация — это не волшебная таблетка. Она требует больше таблиц, больше соединений в запросах и сложнее в поддержке. Вопрос не в том, нормализовать или нет, а в том, до какой степени это делать и когда отступать.

Первые три нормальные формы без учебника

Первая нормальная форма: одно значение в ячейке

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

Плохо:

Заказы:
| 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                  |

Но за это придётся платить: при каждом изменении заказов нужно обновлять счётчик в таблице клиентов. Если этого не сделать, данные станут несогласованными. Дублирование данных — это компромисс между производительностью и согласованностью.

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

Признак, по которому видно, что отступили рано — отсутствие измерений. Если вы денормализовали схему, потому что “так быстрее”, но не измерили, насколько быстрее, и не сравнили с альтернативами (например, с индексами), то вы отступили рано. Денормализация без измерений — это гадание на кофейной гуще.

Позиция редакции

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

Нормализация неверна, если:

  • данные никогда не обновляются (например, логи или архивные записи);
  • дублирование не приводит к несогласованности (например, неизменяемые справочники);
  • производительность важнее согласованности (например, в аналитических системах).

Но даже в этих случаях нормализация может быть полезна. Например, в аналитических системах данные часто обновляются редко, но их объём огромен. Нормализация может помочь сэкономить место на диске и ускорить загрузку данных. Всё зависит от конкретной задачи и конкретных данных.

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

Совпадение индекса с запросом и его отсутствиеИндекс подходитИндекс не подходит
Предыстория

Индексы в базе: какие ставить и когда они не спасают

Индексы — основа оптимизации запросов: как они экономят время и ресурсы, когда работают и почему иногда бесполезны.

Сравнение автоинкремента и UUID: разделённое поле с пересекающими границу элементамиАвтоинкрементUUID
Практика

UUID или автоинкремент: чем платят за каждый

Какой первичный ключ выбрать для базы, чтобы не проиграть в скорости, масштабировании и удобстве работы — разбираемся на примерах автоинкремента, UUID и других вариантов.

Время съедает один шаг плана выполнения, а не запрос целиком: искать надо его, а не вешать индекс наугадшаги планадорогой шаг
Углубление

Медленный запрос в PostgreSQL: с чего начинать

Как диагностировать медленный запрос в PostgreSQL без лишних индексов: смотрим статистику, план выполнения и условия перед оптимизацией.

Письмо по вторникам

Один разбор недели и короткий список того, что изменилось. Без дайджестов на сорок ссылок.

Начните вводить — материалы появятся здесь.

выбрать · Enter открыть · Esc закрыть