Представьте: вы удаляете последний заказ клиента из базы — и вместе с ним исчезают все его контактные данные. Или меняете адрес поставщика, и приходится править его в сотне строк, потому что он продублирован в каждом заказе. Это не баг вашей логики — проблема в непродуманной структуре базы данных.
Нормализация данных — это процесс приведения таблиц к виду, где каждый факт хранится ровно один раз. Думайте о ней как об уборке шкафа: вместо того чтобы хранить носки во всех ящиках подряд, вы выделяете отдельный ящик только для них.
В этом материале мы пройдем путь нормализации поэтапно: детально остановимся на трех базовых формах (1НФ, 2НФ, 3НФ), проиллюстрировав их понятными примерами. Остальные формы (БКНФ, 4НФ, 5НФ и 6НФ) мы затронем обзорно — чтобы вы знали об их существовании и понимали, в каких случаях они могут пригодиться.
Что такое нормализация данных простыми словами
Определение нормализации данных: это систематический процесс организации данных в реляционной базе, который минимизирует избыточность и устраняет аномалии при работе с данными.
Представьте, что вы ведете учет серверной инфраструктуры в Excel. Допустим, таблица выглядит так:
| Дата инцидента | Инженер | Email инженера | Сервер | ОС |
| 24.08 | Анна | anna@devops.io | web-01 | Ubuntu 22.04 |
| 24.08 | Анна | anna@devops.io | db-01 | Ubuntu 22.04 |
При смене фамилии или почты Анны вам придется обновлять данные в обеих строках. Пропустите одну — получите рассогласование в базе. Нормализация говорит: «Вынеси инженеров в отдельный справочник, а в инцидентах храни только engineer_id».
Зачем нужна нормализация БД
Нормализация в базах данных позволяет:
- устранить аномалии обновления — не нужно менять одни данные в сотне мест;
- избежать потери информации при удалении записей;
- добавлять данные без создания фиктивных связанных записей;
- сократить объем хранилища за счет удаления дублей.
Три типа аномалий: что ломается без нормализации
1. Аномалия модификации
В таблице «Деплои» вы храните имя разработчика и его корпоративный email в каждой строке развертывания. Когда разработчик меняет фамилию или почтовый домен, вам нужно обновить эти данные в каждом его деплое. Пропустите хотя бы одну запись — и в базе появятся противоречивые данные, а потом станет непонятно, какой контакт актуален.
2. Аномалия удаления
Инженер Игорь сделал всего один коммит в репозиторий, после чего проект закрыли, а запись о коммите удалили. Вместе с ней исчезла вся информация об Игоре (должность, отдел, навыки), потому что она была привязана исключительно к этому коммиту.
3. Аномалия вставки
Вы приняли нового системного администратора в штат, но он пока не сделал ни одного деплоя и не зарегистрировал ни одного сервера. Если схема таблицы жестко требует наличия server_id для добавления строки, вы просто физически не сможете внести его в базу до первого развертывания.
Какие бывают нормальные формы БД и как они работают
В теории баз данных используют несколько нормальных форм — 1НФ–6НФ, а также БКНФ и ДКНФ. Каждая из них задает свои требования к зависимостям и структуре данных. Более высокая нормальная форма обычно предполагает выполнение требований предшествующих форм.
| Уровень | Форма | Что устраняет | Как проверить себя (признак нарушения) |
| 1 | 1НФ | Списки в ячейках | Есть ли столбец, который я парсю через split или explode перед использованием? |
| 2 | 2НФ | Частичные зависимости от ключа | Есть ли составной ключ, при котором неключевой столбец зависит только от части ключа? |
| 3 | 3НФ | Транзитивные зависимости | Зависит ли столбец С от столбца В, который не является ключом? |
| 3.5 | БКНФ | Детерминанты, не являющиеся суперключами | Есть ли функциональная зависимость, в которой детерминант не является суперключом? |
| 4 | 4НФ | Многозначные зависимости | Есть ли в таблице независимые списки фактов, относящихся к одной сущности? |
| 5 | 5НФ | Зависимые соединения | Можно ли декомпозировать таблицу и восстановить исходные данные соединением без потери информации и появления лишних комбинаций? |
| 6 | 6НФ | Предельная декомпозиция отношений | Нужно ли хранить историю изменения каждого атрибута как отдельный факт во времени? Этот вопрос особенно полезен в работе с темпоральными БД. |
На практике подавляющее большинство прикладных задач закрываются тремя первыми нормальными формами — именно их мы разберем максимально подробно. К более высоким формам обращаются, когда предметная область требует более строгой декомпозиции данных или содержит нетривиальные пересечения сущностей.
Итак, начнем с самого основания — первой нормальной формы.
1НФ: одна ячейка = одно значение
Первая нормальная форма требует, чтобы в каждой ячейке таблицы хранилось только одно атомарное (неделимое) значение. Никаких списков через запятую, массивов или нескольких значений в одной ячейке.
Правило 1НФ: если все атрибуты отношения являются простыми и имеют единственное значение, то отношение находится в первой нормальной форме.
Важное уточнение: атомарность — понятие относительное. Она зависит от того, как вы используете данные. Например, телефонный номер +7 (999) 000-11-22 можно считать атомарным, если вы звоните по нему целиком. Но если вам нужно часто искать клиентов по коду оператора (999), то номер стоит разбить на отдельные поля (код_страны, код_оператора, номер). Ориентируйтесь не на математическую неделимость, а на логику ваших запросов.
Представьте таблицу «Стек технологий проекта»:
| Проект | Используемые языки |
| Веб-портал | Python, JavaScript, Go |
| Мобильное API | Java, Kotlin |
| Аналитика | Python, R, SQL |
В чем проблема? В одной ячейке перечислено несколько языков. Вы не сможете найти все проекты, где используется Go, простым запросом WHERE language = ‘Go’ — придется использовать LIKE ‘%Go%’, что усложняет точный поиск и обработку отдельных значений.
Чтобы привести к 1НФ, разворачиваем каждую технологию в отдельную строку:
| Проект | Язык |
| Веб-портал | Python |
| Веб-портал | JavaScript |
| Веб-портал | Go |
| Мобильное API | Java |
| Мобильное API | Kotlin |
| Аналитика | Python |
| Аналитика | R |
| Аналитика | SQL |
Теперь каждая ячейка содержит ровно один язык — таблица в 1НФ.
Как проверить свою таблицу
Задайте себе вопросы:
- Использую ли я функции разделения строк для получения значений?
- Храню ли в одном поле несколько email-адресов через запятую?
- Есть ли у меня столбцы типа «навыки» со значениями «Python, SQL, Docker»?
Если да — таблица не в 1НФ.
2НФ: факт зависит от всего ключа, а не от части
Вторая нормальная форма требует, чтобы таблица находилась в 1НФ и не содержала частичных зависимостей. Если ключ составной, каждый неключевой атрибут должен зависеть от всего ключа, а не только от его части.
Правило 2НФ: отношение находится во второй нормальной форме, если оно находится в 1НФ и каждый неключевой атрибут неприводимо зависит от всего первичного ключа.
Представим таблицу «ПО на серверах» с составным ключом {server_id, package_name}:
| server_id | package_name | owner | owner_phone | version |
| 101 | nginx | Алексей | +7-916-111-22-33 | 1.24.0 |
| 101 | postgres | Алексей | +7-916-111-22-33 | 15.2 |
| 202 | nginx | Мария | +7-903-555-66-77 | 1.26.0 |
| 303 | redis | Алексей | +7-916-111-22-33 | 7.0.5 |
Проблемы здесь:
- owner и owner_phone зависят только от server_id (у одного сервера один владелец), а не от пары {server_id, package_name};
- version зависит только от package_name (в этом примере version — актуальная версия пакета в каталоге).
Если актуальная версия nginx в каталоге изменится, придется править версию в каждой строке, где встречается nginx. Забудем одну — получим противоречивые значения для одного пакета.
Приведение к 2НФ
Разбиваем таблицу на три:
-- Таблица серверов (владелец хранится здесь)CREATE TABLE servers ( server_id INT PRIMARY KEY, owner_name VARCHAR(100), owner_phone VARCHAR(20)); -- Таблица доступных пакетов (актуальная версия)CREATE TABLE packages ( package_id INT PRIMARY KEY, name VARCHAR(100) UNIQUE, current_version VARCHAR(20)); -- Связка серверов и установленных пакетовCREATE TABLE server_installed_packages ( server_id INT REFERENCES servers(server_id), package_id INT REFERENCES packages(package_id), install_date TIMESTAMP, PRIMARY KEY (server_id, package_id));
Если изменить current version для nginx в таблице packages, при запросе через связь для всех записей nginx будет возвращаться новое значение. Данные о владельце теперь хранятся строго в одном месте — servers.
3НФ: без транзитивных связей
Третья нормальная форма устраняет транзитивные зависимости — ситуации, когда неключевой атрибут зависит не от ключа, а от другого неключевого атрибута. Допустим, мы храним данные о репозиториях и командах, которым они принадлежат, в одной таблице:
| repo_name | team | team_lead_email |
| backend-api | Платформа | lead@platform.io |
| frontend-app | Платформа | lead@platform.io |
| data-pipeline | Данные | chief@data.io |
Ключ — repo_name. Но team_lead_email зависит от team, а не напрямую от repo_name:
repo_name → team → team_lead_email (транзитивная зависимость).
Если у команды «Платформа» сменится тимлид, придется обновлять email в каждой строке, где встречается эта команда. Пропустите одну — получите рассогласование.
Выносим команды в отдельную таблицу:
CREATE TABLE teams ( team_id INT PRIMARY KEY, name VARCHAR(100) UNIQUE, lead_email VARCHAR(255)); CREATE TABLE repos ( repo_id INT PRIMARY KEY, name VARCHAR(255) UNIQUE, team_id INT REFERENCES teams(team_id));
Теперь смена почты тимлида — это правка одной строки в teams, а актуальный контакт для репозиториев можно получить через связь с этой таблицей.
Что дальше: БКНФ, 4НФ, 5НФ и 6НФ
Высшие нормальные формы нужны для сложных сценариев.
Нормальная форма Бойса-Кодда (БКНФ / BCNF)
Она нужна, когда существует нетривиальная функциональная зависимость, в которой детерминант не является суперключом (это один или несколько атрибутов таблицы, которые однозначно идентифицируют каждую строку). БКНФ требует, чтобы каждый детерминант (атрибут или набор атрибутов, определяющий другие значения) был суперключом.
Представьте расписание занятий:
| Студент | Предмет | Преподаватель |
| Иван | Математика | Петров |
| Иван | Физика | Сидоров |
| Анна | Математика | Петров |
Здесь есть правила:
- {Студент, Предмет} → Преподаватель.
- Преподаватель → Предмет (в этом примере каждый преподаватель ведет только один предмет).
Здесь два кандидатных ключа: {Студент, Предмет} и {Студент, Преподаватель}. Зависимость «Преподаватель → Предмет» не нарушает 3НФ, потому что Предмет входит в кандидатный ключ, но нарушает БКНФ: Преподаватель не является суперключом. Для приведения к БКНФ отношение можно разложить на «Преподаватели — Предметы» и «Студенты — Преподаватели».
Такие нарушения проверяют отдельно после 3НФ. Если схема находится в 3НФ, она может удовлетворять и БКНФ, но это нужно отдельно проверить по функциональным зависимостям.
4НФ: многозначные зависимости
Она нужна, когда один атрибут независимо связан сразу с несколькими наборами значений. Такая ситуация называется многозначной зависимостью: изменение одного набора не должно влиять на другой.
Приведем пример. Ресторан предлагает несколько видов пиццы и доставляет заказы в несколько районов. Эти факты не зависят друг от друга: добавление новой пиццы не означает, что ресторан начал доставлять в новый район. Если хранить все данные в одной таблице, придется создавать отдельную строку для каждой комбинации пиццы и района. Например, у ресторана 5 видов пиццы и 4 района доставки — таблица будет содержать 20 строк. Если добавить еще одну пиццу, появятся 4 новые записи, хотя информация о районах доставки не изменилась. Решение — разделить данные на две таблицы: «Ресторан + пицца» и «Ресторан + район». Тогда новый вид пиццы добавляется одной записью, а район доставки не дублируется. Это уменьшает избыточность и снижает риск ошибок при обновлении данных.
4НФ становится актуальной, когда в одной таблице хранят несколько независимых многозначных связей.
5НФ: зависимые соединения
5НФ нужна для редких бизнес-сценариев, где одна таблица хранит связь сразу между тремя и более сущностями, а допустимые комбинации этих сущностей определяются несколькими независимыми связями. 5НФ устраняет избыточность, которую не удается убрать с помощью предыдущих нормальных форм. Такая ситуация называется зависимостью соединения (join dependency).
Например, компания работает с поставщиками, деталями и проектами. В одной таблице можно хранить три поля — Поставщик, Деталь, Проект. При этом действует бизнес-правило: если поставщик работает с деталью, поставляет что-либо для проекта и эта деталь используется в проекте, то поставщик может поставлять эту деталь для данного проекта. В таком случае информацию можно разделить на три таблицы: «Поставщик — Деталь», «Поставщик — Проект» и «Деталь — Проект». Исходные данные затем восстанавливаются соединением этих таблиц без потери информации.
Без такого разделения одна и та же связь может повторяться во множестве строк. Разбиение уменьшает избыточность и снижает риск противоречий при изменении данных. При этом 5НФ увеличивает число таблиц и усложняет запросы, поэтому ее применяют там, где такие зависимости действительно есть.
На практике 5НФ встречается крайне редко. Для большинства прикладных систем достаточно нормализации до 3НФ или BCNF, а более высокие формы используют при специфической структуре данных и строгих бизнес-правилах.
6НФ и ДКНФ
6НФ — нормальная форма, при которой отношение не допускает дальнейшей нетривиальной декомпозиции без потерь. Такой подход особенно часто рассматривают в контексте темпоральных баз данных, где важно хранить историю изменений и знать, какое значение действовалкогда конкретный период.
Например, у сотрудника меняется должность. Вместо того чтобы просто перезаписать значение Должность, база хранит историю: «аналитик — с 1 января по 30 июня», «старший аналитик — с 1 июля». Это позволяет получить состояние данных на любую дату и отследить изменения во времени. 6НФ особенно актуальна для систем, где история изменений — одна из основных задач.
ДКНФ (доменно-ключевая нормальная форма) — нормальная форма, в которой все ограничения на данные должны следовать только из доменов (допустимых значений атрибутов) и ключей. Например, если поле Возраст допускает только целые значения от 0 до 120, это ограничение задается его доменом, а уникальность идентификатора — ключом.
Сравнение: какую форму выбрать
Денормализация: когда нормализация мешает
Нормализация — не самоцель. Она уменьшает дублирование и помогает поддерживать целостность данных, но иногда большое число связей и JOIN усложняют или замедляют запросы. В таких случаях данные осознанно дублируют — это и есть денормализация.
Когда применять денормализацию
- Исторические данные (snapshot)
Цена товара в каталоге может измениться, но в оплаченном заказе должна остаться цена на момент покупки. Поэтому price хранят непосредственно в order_items, даже если актуальную цену можно получить из таблицы товаров. Это отдельный исторический факт: цена конкретной позиции заказа на момент покупки.
- Готовые агрегаты
Сумму заказа можно каждый раз вычислять через SUM() по его позициям. Но если приложение обрабатывает тысячи запросов в секунду, постоянный пересчет может создавать лишнюю нагрузку. Тогда total_amount сохраняют в orders и обновляют при изменении состава заказа.
- Аналитические витрины
В OLAP-системах данные часто хранят в широких денормализованных таблицах. Например, в одной витрине можно сразу разместить дату, товар, категорию, регион, клиента и показатели продаж. Аналитический запрос получает нужные данные с меньшим числом JOIN, что упрощает расчеты и может ускорить чтение больших объемов данных.
- Кэширование
Redis и другие системы кэширования часто хранят данные в форме, удобной для конкретного сценария чтения. Например, профиль пользователя или результат сложного запроса можно сохранить целиком, чтобы не собирать его заново из нескольких таблиц. Для кэша такая денормализация — нормальная практика.
Что важно учитывать
Денормализация увеличивает ответственность за актуальность дублируемых данных. Если одно значение хранится в нескольких местах, нужно обеспечить их синхронизацию через код приложения, триггеры, ETL/ELT-процессы или другие механизмы. Поэтому перед денормализацией стоит оценить выигрыш в скорости чтения и сравнить его с дополнительной сложностью поддержки данных.
Частые ошибки при нормализации
Нормализация БД — коротко о главном
Нормализация базы данных — это процесс организации таблиц для уменьшения и предотвращения аномалий.
Ключевые моменты:
- 1НФ — одна ячейка = одно значение. Никаких списков через запятую.
- 2НФ — нет частичных зависимостей неключевых атрибутов от составного ключа.
- 3НФ — нет транзитивной зависимости неключевого атрибута от ключа через другой неключевой атрибут.
- БКНФ, 4НФ, 5НФ, 6НФ — применяются в более специфических моделях данных.
- Денормализация оправдана для снимков состояния, агрегатов и аналитики.
Практический ориентир: нормализуйте до 3НФ → измеряйте производительность → денормализуйте только там, где это дает измеримый выигрыш.
Нормализация — не религия, а инженерный инструмент. Лучшая схема — та, которая решает вашу задачу, а не та, что набрала максимум нормальных форм.
