Введение
Про "нормальные формы" чаще всего вспоминают либо на собеседованиях по направлениям дата-инженерии и аналитики, связанной с проектированием КХД, либо в более приземленных спорах о том, насколько "правильно" спроектирована та или иная модель в хранилище, которое используется в проекте. Нормализация - это не набор абстрактных правил ради теории, а способ понять, как именно раскладывать данные по структуре так, чтобы одни и те же факты не дублировались без необходимости, не противоречили друг другу и не создавали лишние проблемы при развитии хранилища.
В этой статье я хочу разобрать основные нормальные формы и показать, как нормализация помогает проектировать структуры данных для корпоративных хранилищ: от сырых и избыточных таблиц к моделям, где бизнес-сущности, атрибуты, связи и зависимости разделены более осознанно.
Начнем с простого, что такое "нормальная форма" и почему она так называется?
Под "нормальной формой" обычно понимают некоторое правило или набор правил, которым должна соответствовать структура данных, чтобы считаться более упорядоченной с точки зрения логики хранения.
Само слово "нормальная" здесь не означает, что все остальное "ненормально". Речь скорее о некоторой эталонной структуре, приведенной к более правильному виду, в которой устранена часть избыточности, зависимостей и потенциальных аномалий при вставке, обновлении или удалении данных.
Иными словами, каждая следующая нормальная форма - это попытка еще немного очистить и упорядочить модель.
Небольшая историческая вводная и связь с математической теорией
Хочу отметить важную деталь, что нормальные формы родились не из "бытовой практики" DBA-специалистов, а из более ранней и довольно строгой реляционной теории, которая скорее относится к математике. Основу этой линии заложил Эдгар Ф. Кодд, опубликовавший в 1970 году работу "A Relational Model of Data for Large Shared Data Banks", где реляционная модель прямо опиралась на теорию отношений. Кстати говоря, одна из нормальных форм, BCNF, как раз в своем названии имеет фамилию ученого. Если подойти более четко, то нормальные формы опираются на два математических основания:
- теорию множеств (с их отношениями)
- и логику предикатов первого порядка (как основу для формального описания объектов, связей и зависимостей данных)
Какие нормальные формы существуют, и почему они так выстроены?
Классическая линия нормализации обычно выглядит так: "1НФ, 2НФ, 3НФ, BCNF, 4НФ, 5НФ, DKNF и 6НФ".
Они выстроены так не случайно, а как последовательные уровни ужесточения требований к структуре данных. Сначала убираются самые грубые проблемы вроде: неатомарных значений и повторяющихся групп, транзитивных зависимостей, а в конце - более тонкие и редкие аномалии, связанные с многозначными зависимостями, join-зависимостями и максимально-дробной декомпозицией структуры данных.
Что такое "BCNF и DKNF"? Почему они имеют иное название без цифры в названии? Почему имеют такое местоположение в последовательности?
BCNF - это нормальная форма Бойса-Кодда (Boyce-Codd Normal Form). Она названа не по номеру, а по фамилиям исследователей, потому что исторически появилась, как отдельное уточнение и усиление 3НФ, а не просто как следующая по счету ступень.
DKNF - это доменно-ключевая нормальная форма (Domain-Key Normal Form). У нее тоже не числовое, а смысловое название, потому что она определяется через другой принцип, что все ограничения должны вытекать только из доменов и ключей без дополнительных специальных ограничений. В теоретической шкале ее обычно ставят после 5НФ, как еще более строгий уровень нормализации, но на практике ее разбирают заметно реже, потому что она считается в большей степени теоретической и редко дает ощутимую прикладную пользу в обычном проектировании.
Далее давайте разберем каждую нормальную форму в отдельности, включая момент, когда нормализации фактически еще нет.
Отсутствие нормализации, как это выглядит по данным?
Прежде чем переходить к конкретным нормальным формам, полезно зафиксировать точку, с которой все обычно начинается - ситуацию, когда нормализации по сути еще нет, т.е. корректнее всего здесь говорить не о "нулевой нормальной форме" как о полноценной форме, а о ненормализованной структуре, в которой данные уже собраны и даже пригодны для хранения или первичной загрузки, но еще не приведены к более упорядоченному виду.
Ремарка. Если подходить к теме строго теоретически (теория множеств), то отношение в реляционной модели уже считается находящимся в 1НФ. В этой статье под состоянием "до 1НФ" я буду иметь в виду не отношение в строгом смысле, а сырую или ненормализованную структуру данных, с которой обычно и начинается практический разбор.
На практике это часто выглядит, как:
- одна плоская таблица
- JSON-документ или сырая выгрузка, куда складывают сразу несколько разных сущностей и повторяющихся значений
Например, в одной записи могут одновременно лежать:
- клиент
- его телефоны
- список товаров
- адрес доставки
- менеджер
- название отдела менеджера
- история статусов заказа
Для первичной загрузки или обмена данными такой формат может быть удобен, но по факту это отсутствие нормализации, потому что сильно перемешаны все возможные связи, и где можно выделить ряд данных в отдельные сущности, вместо этого у нас "как бы помойка" на входе в контексте иерархии и декомпозиции данных.
Проблема здесь не в том, что "данные плохие" сами по себе, а в том, что структура еще не разделяет атомарные значения, повторяющиеся группы и разные сущности. Именно с такого состояния обычно и начинается переход к нормализации: сначала убираются самые грубые смешения и повторения, а затем модель шаг за шагом становится более строгой.
Первая нормальная форма (1НФ), что она улучшает по сравнению с предыдущим шагом?
Если на предыдущем шаге данные еще выглядели, как ненормализованная структура, где в одной записи могли одновременно лежать много данных, которые явно подлежат декомпозиции по отношениям и иерархии, то 1НФ делает первый минимальный шаг к порядку, она требует, чтобы значения стали более атомарными, а повторяющиеся группы перестали храниться в одном поле или внутри одной строки как список.
Проще говоря, если раньше в одной записи лежат, например, сразу несколько телефонов клиента или несколько товаров заказа, то в 1НФ такие значения уже нужно разносить так, чтобы одно поле содержало - одно значение, а повторения оформлялись как отдельные строки или отдельные связанные сущности.
Пример до 1НФ:
В чем проблема?
- в phones лежит сразу несколько значений
- в products тоже лежит список
- одна строка хранит сразу несколько повторяющихся групп
Давайте сделаем нормализацию неатомарного поля products:
Что улучшилось?
- в одном поле теперь лежит одно значение
- списки исчезли
- структура стала более предсказуемой для хранения и обработки
Однако одной только атомарности значений еще недостаточно. Даже если вложенные списки уже устранены, это еще не означает, что структура стала логически чистой. Внутри нее все еще могут оставаться поля, которые относятся не ко всей записи целиком, а только к части ее ключа. Именно это и подводит нас ко второй нормальной форме.
Вторая нормальная форма (2НФ), что она улучшает по сравнению с 1НФ?
Если 1НФ убирает списки внутри полей, то 2НФ проверяет следующий уровень порядка: относятся ли остальные данные ко всей записи целиком, а не только к одной ее части. Иначе часть полей все еще будет храниться не на своем месте.
Иными словами, каждое поле в строке должно описывать именно всю эту строку, как единый факт, а не только один кусок этого факта.
На условном примере это выглядит так:
- если строка говорит про "товар в заказе", то остальные поля должны относиться именно к факту "этот товар в этом заказе"
- если какое-то поле относится только к заказу или только к товару, значит оно описывает не всю строку, а только ее часть
На следующем примере строка таблицы описывает товар в заказе. Списков внутри полей уже нет, но имя клиента все еще дублируется в каждой строке, хотя по смыслу относится ко всему заказу:
Для приведения к 2НФ данные о заказе и данные о товарах внутри заказа нужно разделить. Имя клиента относится ко всему заказу, а не к каждой товарной строке, поэтому оно должно храниться отдельно. В результате дублирование исчезает, а структура становится чище:
Ремарка. В этом упрощенном примере после разбиения структура уже выглядит не только как 2НФ, но и как более строгая форма. И это нормально, т.к. на простых учебных примерах устранение проблемы 2НФ часто сразу убирает и часть следующих проблем. Однако в реальных моделях данные о клиенте обычно выделяются в отдельную сущность, тогда как в нашем упрощенном примере customer_name по-прежнему хранится прямо в таблице orders.
Итог по 1НФ и 2НФ. 1НФ отвечает на вопрос: как именно хранятся значения, по одному в поле или в виде списков и вложенных наборов. 2НФ отвечает уже на другой вопрос: относятся ли остальные поля ко всей записи как к единому факту, а не только к одной ее части. Иными словами, 1НФ наводит базовый порядок в форме хранения, а 2НФ - в логике самой записи.
Но и этого все еще недостаточно. Даже если данные о заказе уже отделены от данных о товарных строках, внутри самой таблицы заказа могут оставаться поля, которые относятся не к заказу напрямую, а к другой сущности, например к клиенту. Именно с этой точки уже удобно перейти к 3НФ и посмотреть на пример, где сущность customer выделена явно.
Третья нормальная форма (3НФ), что она улучшает по сравнению с 2НФ?
При обзоре 2НФ мы уже отделили данные заказа от данных его товарных строк. Но этого все еще недостаточно, потому что внутри самой таблицы заказа могут оставаться поля, которые относятся уже не к заказу, а к связанному с ним объекту - например, к клиенту. Именно это и исправляет 3НФ, решая проблему транзитивной зависимости. Форма требует, чтобы в таблице оставались данные о самой сущности, а сведения о связанных сущностях выносились отдельно.
Проще говоря, 2НФ разделяет части одной записи, а 3НФ разделяет уже сами сущности. Если таблица заказа хранит не только order_id и customer_id, но и customer_name, то имя клиента оказывается привязано к заказу, хотя по смыслу относится не к заказу, а к клиенту.
- пример прямой зависимости от ключа: order_id => order_date
- через призму другого поля: order_id => customer_id => customer_name
И тогда дизайн 3НФ-таблиц будет иметь следующий вид:
Почему с 3НФ обычно связывают понятие транзитивной зависимости? Потому что именно на этом уровне нормализации становится важно не только то, как данные хранятся внутри записи, но и через какие промежуточные поля одни атрибуты оказываются связанными с ключом. Если поле зависит от ключа не напрямую, а через другое неключевое поле, это обычно означает, что в таблицу попали данные уже другой сущности. Именно такие случаи 3НФ и старается устранять.
Нормальная форма Бойса-Кодда (BCNF), чем она строже 3НФ?
BCNF обычно рассматривают как усиленную 3НФ. На практике разница между ними ощущается слабее, чем между 2НФ и 3НФ, потому что BCNF не вводит совсем новый тип проблемы, а скорее дочищает редкие случаи, которые 3НФ еще может пропустить. Если говорить совсем просто, BCNF убирает ситуации, где одно поле внутри таблицы уже само по себе определяет другое, и из-за этого часть данных хранится лишний раз.
Если совсем кратко, BCNF - это 3НФ без оставшихся логических "лазеек". Если таблица уже выглядит нормальной, но внутри нее одно поле все еще однозначно определяет другое, BCNF потребует вынести такую зависимость отдельно.
Чтобы для демонстрации получить случай для BCNF, можно добавить не новую сущность, а второй способ идентификации того же самого товара. Например, помимо поля product в строке заказа хранить еще и поле product_code:
Строка по-прежнему описывает тот же самый факт - товар в заказе, то есть мы не ломаем уже выстроенную логику 3НФ про иную сущность. Однако внутри самой строки появляется более тонкая зависимость product_code => product. Если приводить такую структуру к BCNF, то нормализация будет выглядеть так:
Четвертая нормальная форма (4НФ), что она улучшает?
Если в BCNF строка все еще хранит один факт, но с лишней внутренней зависимостью, то в 4НФ проблема уже другая: одна строка начинает искусственно склеивать два независимых факта. Например, ресторан отдельно готовит разные виды пиццы и отдельно доставляет заказы в разные районы.
Если попытаться хранить это в одной таблице вида: restaurant_id | pizza | delivery_zone, то строка начинает выглядеть как единый факт, хотя на деле это просто пересечение двух независимых списков (т.е. двух независимых фактов):
Именно это и исправляет 4НФ: такие данные нужно разносить отдельно, например в restaurant_pizzas(restaurant_id, pizza) и restaurant_zones(restaurant_id, delivery_zone):
Пятая нормальная форма (5НФ), что она улучшает?
Если 4НФ убирает смешение независимых фактов в одной таблице, то 5НФ идет еще дальше и разбирает случаи, где одна таблица хранит уже слишком сложный составной факт, который на первый взгляд выглядит цельным:
Смысл здесь в том, что такой факт можно без потерь собрать из нескольких более простых таблиц, а сама исходная таблица не добавляет к ним новой логики. Иначе говоря, 5НФ нужна там, где одна общая связка выглядит цельной, но на деле оказывается просто результатом соединения нескольких более простых связей. Нормализация такой таблицы до 5НФ будет выглядеть так:
Доменно-ключевая нормальная форма (DKNF), что она улучшает?
Если 5НФ все еще говорит в основном о том, как разложить слишком сложную связь, то DKNF делает шаг еще в сторону общей логической чистоты модели. Смысл здесь уже не столько в очередной декомпозиции таблиц, сколько в том, чтобы структура не опиралась на дополнительные частные правила, исключения и скрытую бизнес-логику.
Почему она называется доменно-ключевой?
Потому что она описывает модель, где все правила можно объяснить только двумя вещами:
- какие значения допустимы в полях
- и какие поля образуют уникальную запись
Если для корректности нужны еще какие-то отдельные специальные условия, то это уже не DKNF.
Шестая нормальная форма (6НФ), что она улучшает?
6НФ доводит декомпозицию до предела, одно независимое свойство хранится отдельно от других. Поэтому она особенно хорошо ложится на хронологические данные, где разные свойства одной сущности меняются в разное время и их удобнее хранить раздельно.
На той же логике строится и якорная модель. Она является достаточно масштабируемой логической моделью КХД, где якорь задает сущность, атрибуты хранят свойства по отдельности, а связи выносятся отдельно. Данные раскладываются по таблицам-атрибутам, где обычно хранится одно атомарное значение одного свойства. Более подробно изучить методологию якорного моделирования можно по следующим ссылкам:
Заключение
Нормальные формы хорошо воспринимать не как "сухую теоретическую лестницу", а как последовательные шаги наведения порядка в данных. Это достаточно важно при построении КХД.
Лично мне импонирует якорная модель с ее максимальной степенью нормализации, но на практике она не всегда хорошо ложится на физические реалии. К примеру, MPP РСУБД, такие как Greenplum, с AO/AOCO-таблицами не очень хорошо работают с такой логической моделью, но это тема для отдельной статьи, которую я думаю написать позже.