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

Естественно, можно спроектировать базу данных, вообще не заботясь ни о каких правилах. И она даже будет работать! Все будет прекрасно ровно до первого ее реального использования в продакшене. При проектировании «абы-как» возникают три типовые проблемы: избыточность, аномалии обновления, аномалии удаления.

И вот это уже плохо.

Избыточность данных приводит к засорению базы данных повторяющимися данными. При необходимости правки придется изменять данные во всех местах, где они повторяются. Один раз забыл и привет несогласованность.

В рамках этой статьи мы будем работать с реляционными базами данных.

Взглянем на пример:

ID предмета

Наименование предмета

Материал

1

Стул

Металл

2

Стол

Массив дерева

3

Кровать

ЛДСП

4

Шкаф

Массив дерева

5

Комод

ЛДСП

В этом примере в столбце Материал массив дерева и ЛДСП повторяется несколько раз. Повторение одного и того же значения в нескольких строках и создает избыточность данных.

Аномалии обновления 

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

Пример:

Студент

Курс

Преподаватель

Платформа

Иван

Python

Петров

Платформа А

Мария

Python

Петров

Платформа А

Петр

С++

Иванова

Платформа Б

Анна

С++

Иванова

Платформа Б

Иван

С++

Иванова

Платформа Б

Допустим, платформа А провела ребрендинг и теперь называется Платформа В. Нам нужно найти все упоминания старого названия и заменить на новое. Мы меняем данные, но что-то идет не так, и данные Марии остаются нетронутыми.

Студент

Курс

Преподаватель

Платформа

Иван

Python

Петров

Платформа В

Мария

Python

Петров

Платформа А

Петр

С++

Иванова

Платформа Б

Анна

С++

Иванова

Платформа Б

Иван

С++

Иванова

Платформа Б

Вследствие этой ошибки в БД произошел хаос. Ложь и правда живут вместе, но система даже не подозревает неладное. Все потому что изменение данных должно происходить за одну операцию и во всей БД сразу. Будь у нас миллион строк в нескольких таблицах, каждая из которых содержит избыточную информацию о Платформе, нам бы пришлось изменять их всех. Если где-то забыли – несогласованность. Это и есть аномалия обновления.

Аномалия удаления

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

Взглянем сюда:

Студент

Курс

Преподаватель

Платформа

Иван

Python

Петров

Платформа А

Мария

Python

Петров

Платформа А

Петр

С++

Иванова

Платформа Б

Анна

SQL

Иванова

Платформа Б

Иван

SQL

Иванова

Платформа Б

Допустим студент Петр внезапно решил бросить обучение и отчислился. Его запись удаляется из БД администратором: удаляется строка с Петром.

Студент

Курс

Преподаватель

Платформа

Иван

Python

Петров

Платформа А

Мария

Python

Петров

Платформа А

Анна

SQL

Иванова

Платформа А

Иван

SQL

Иванова

Платформа А

Помимо Петра из БД исчезает информация о существовании курса С++ и платформы Б. Удаленная информация не была связана напрямую с Петром, однако, теперь информация вообще перестала существовать. Своим отчислением Петр заодно закрыл целую программу подготовки.

Облачные базы данных

Создайте готовую базу данных в облаке за 5 минут. Поддерживаем PostgreSQL, MySQL, Redis и не только.

Подробнее →

Декомпозиция и нормализация

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

Декомпозиция без потерь — это разбиение, при котором после обратного соединения (JOIN) мы получаем ровно исходную таблицу без новых строк и без потерь.

Сначала посмотрим на пример неудачного разбиения:

Студент

Курс

Лектор

Иван

Математика

Петров

Иван

Физика

Смирнов

Мария

Математика

Петров

Мария

Информатика

Соколова

Петр

Физика

Смирнов

Петр

Информатика

Соколова

Разделяем эту таблицу на 2:

Студенты_Лекторы

Студент

Лектор

Иван

Петров

Иван

Смирнов

Мария

Петров

Мария

Соколова

Петр

Смирнов

Петр

Соколова

Курсы_Лекторы

Курс

Лектор

Математика

Петров

Физика

Смирнов

Информатика

Соколова

При JOIN соединении этих двух таблиц по полю Лектор и получаем:

Студент

Лектор

Курс

Иван

Петров

Математика

Иван

Смирнов

Физика

Иван

Соколова

Информатика

Мария

Петров

Математика

Мария

Соколова

Информатика

Петр

Смирнов

Физика

Петр

Соколова

Информатика

Петр

Петров

Математика

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

Иван — Информатика и Петр — Математика

Это произошло по причине того, что Лектор не определяет курс однозначно. Один лектор может быть связан с несколькими курсами и с несколькими студентами.

При JOIN произошло перемножение строк (комбинирование всех возможных вариантов) по совпадающему значению Лектор

Разделим немного по-другому:

«Студенты_Курсы»                                            

Студент

Курс

Иван

Математика

Иван

Физика

Мария

Математика

Мария

Информатика

Петр

Физика

Петр

Информатика

 «Курсы_Лекторы»

Курс

Лектор

Математика

Петров

Физика

Смирнов

Информатика

Соколова

Выполняем JOIN по Курс и получаем:

Студент

Курс

Лектор

Иван

Математика

Петров

Иван

Физика

Смирнов

Мария

Математика

Петров

Мария

Информатика

Соколова

Петр

Физика

Смирнов

Петр

Информатика

Соколова

Все получилось в точности как в исходной таблице – нет лишних строк и потерь данных. По Теорема Хита - если некоторая декомпозиция выполняется в соответствии с определенной функциональной зависимостью, то она будет выполнена без потерь. Иначе говоря:

Пусть R(XYZ) является отношением, где X, Y и Z — непересекающиеся множества атрибутов. Если R удовлетворяет функциональной зависимости X \rightarrow Y, то исходная таблица R может быть без потерь восстановлена путем естественного соединения (JOIN) двух ее проекций: первой — содержащей только множества атрибутов X и Y, и второй — содержащей только множества атрибутов X и Z.

Если у вас есть функциональная зависимость столбца X \rightarrow Y и столбец Z, то вы можете разделить таблицу на 2 без потерь:

·   Таблица столбцов XиY

·   Таблица столбцов XиZ

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

Всего существует несколько нормальных форм:

  • Ненормализованная форма или нулевая нормальная форма (UNF)

  • Первая нормальная форма (1NF)

  • Вторая нормальная форма (2NF)

  • Третья нормальная форма (3NF)

  • Нормальная форма Бойса-Кодда (BCNF)

  • Четвертая нормальная форма (4NF)

  • Пятая нормальная форма (5NF)

  • Доменно-ключевая нормальная форма (DKNF)

  • Шестая нормальная форма (6NF)

В реальности используется нормализация вплоть до Нормальной формы Бойса-Кодда. Последующие формы не нашли широкого применения в жизни и существуют в основном в теории.

Первая нормальная форма

Первая нормальная форма (1НФ) – база, с которой все начинается. Условия для приведения таблицы к 1НФ:

  • Не должно быть дублирующих строк

  • В каждой ячейке хранится атомарное значение (нельзя делить дальше без потери смысла)

  • В столбце данные одного типа

  • Устранение списков и массивов в любом их роде

«Пример ненормализованной грязной UNF таблицы»

НомерЧека

Покупатель

Товары

Цена

Кассир

1

Александр

Молоко, Хлеб

80, 50

Анна

2

Дарья

Сок, Кетчуп, Молоко

100, 400, 80

Дмитрий

3

Александр

Пиво

50

Анна

В процессе приведения таблицы к 1НФ разбиваем повторяющиеся группы на отдельные строки.

НомерЧека

Покупатель

Товары

Цена

Кассир

1

Александр

Молоко

80

Анна

1

Александр

Хлеб

50

Анна

2

Дарья

Сок

100

Дмитрий

2

Дарья

Кетчуп

400

Дмитрий

2

Дарья

Молоко

80

Дмитрий

3

Александр

Пиво

50

Анна

Вторая нормальная форма

  • Таблица находится в 1НФ

  • Каждый неключевой атрибут неприводимо зависит от каждого ее потенциального ключа

Слово «неприводимо» означает, что атрибут должен зависеть от всего составного ключа целиком, и никак от его кусочка. Это также называется полная функциональная зависимость. Смотрим на пример:

«Яблоки»

Товар

Филиал

Цена в филиале

Яблоки

Северный

100 руб

Бананы

Южный

110 руб

Бананы

Северный

90 руб

Яблоки

Южный

120 руб

В этой таблице столбцы Товар и Филиал – составной первичный ключ. Цена в филиале не входит в состав ключа и зависит сразу от 2 значений ключа. По названию товара нельзя получить значение цены, как и по названию филиала. Только зная значения полного ключа мы сможем получить цену товара в конкретном филиале.

Переходим к нормализации.

Для начала введем составной первичный ключ для нашей 1НФ. Чтобы находится в 2НФ, отношение должно быть организовано так, чтобы каждый его неключевой атрибут зависел от полного набора атрибутов потенциального ключа. Нарушение этого условия приведет к избыточности (и не только).

«Чек_1NF»

НомерЧека

Товар

Покупатель

Цена

Кассир

1

Молоко

Александр

80

Анна

1

Хлеб

Александр

50

Анна

2

Сок

Дарья

100

Дмитрий

2

Кетчуп

Дарья

400

Дмитрий

2

Молоко

Дарья

80

Дмитрий

3

Пиво

Александр

50

Анна

Здесь НомерЧека и Товар это составной первичный ключ. Неключевые атрибуты должны зависеть от всего составного ключа(и от НомерЧека и от Товар), однако в нашем случае это не выполняется.

  • Покупатель зависит от НомерЧека (чтобы узнать покупателя хватит только номера чека)

  • Кассир зависит от НомерЧека

  • Цена зависит от Товар

Для устранения этих частичных зависимостей создадим дополнительные таблицы

«Покупатели»

ID_Покупателя

Имя

1

Александр

2

Дарья

«Кассиры»

ID_Кассира

Имя

1

Анна

2

Дмитрий

Также выносим в отдельную таблицу атрибуты зависящие только от НомерЧека

«ЗаголовкиЧеков»

НомерЧека

ID_Покупателя

ID_Кассира

1

1

1

2

2

2

3

1

1

Чтобы цена зависела от товаров, а не от чека выносим товары в отдельную таблицу

«Товары»

ID_Товара

Товар

Цена

1

Молоко

80

2

Хлеб

50

3

Сок

100

4

Кетчуп

400

5

Пиво

50

«Чеки и товары»

НомерЧека

ID_Товара

1

1

1

2

2

3

2

4

2

1

3

5

После всех махинаций мы пришли к 2НФ. Ура! Но это еще не предел.

Третья нормальная форма

  • Таблица находится в 2НФ

  • Не содержит транзитивных зависимостей между неключевыми атрибутами и первичным ключом

По удачным обстоятельствам почти вся наша БД после приведения к 2НФ автоматически стала 3НФ и местами даже BCNF.

Единственный нюанс — это таблица «Товары». Причина станет понятна ниже.

А сейчас немного изменим пример, чтобы наглядно показать разницу между 2НФ и 3 НФ. Для товаров добавим поставщиков и их города.

«Товары и поставщики_2NF»

ID_Товара

Товар

Цена

Поставщик

Город_поставщика

1

Молоко

80

ООО “Ромашка”

Москва

2

Хлеб

50

ООО “Ромашка”

Москва

3

Сок

100

ООО “Букашка”

Санкт-Петербург

4

Кетчуп

400

ООО “Цветочек”

Казань

5

Пиво

50

ООО “Букашка”

Санкт-Петербург

Наблюдаем появление транзитивной зависимости:
ID_Товара → Поставщик → Город_поставщика

В 3НФ не должно быть транзитивных зависимостей. Решаем эту проблему вынося Поставщик и Город_поставщика в отдельную таблицу.

«Товары и поставщики_3NF»

ID_Товара

Товар

Цена

ID_Поставщика

1

Молоко

80

1

2

Хлеб

50

1

3

Сок

100

2

4

Кетчуп

400

3

5

Пиво

50

«Поставщики»

ID_Поставщика

Поставщик

Город

1

ООО “Ромашка”

Москва

2

ООО “Букашка”

Санкт-Петербург

3

ООО “Цветочек”

Казань

Поздравляю, 3НФ достигнута. Теперь вам стало понятно, почему таблицы «Товары» из прошлого примера не совсем соответствовала 3НФ.

Напомню как выглядит таблица «Товары»

«Товары»

ID_Товара

Товар

Цена

1

Молоко

80

2

Хлеб

50

3

Сок

100

4

Кетчуп

400

5

Пиво

50

Мы можем увидеть транзитивную зависимость вида ID_Товара → Товар → Цена

Транзитивная зависимость ID_Товара → Товар → Цена возникает только в том случае, если выполняется функциональная зависимость Товар → Цена. Если одному значению атрибута Товар может соответствовать несколько разных значений Цена, то функциональная зависимость Товар → Цена не выполняется. В этом случае транзитивной зависимости нет, и таблица находится в 3НФ.

Переходим к нормальной форме Бойса-Кодда.

Нормальная форма Бойса-Кодда (НФБК)

  • Таблица находится в 3НФ

  • Каждый детерминант должен быть суперключом

В 3НФ допускаются ситуации, когда детерминант (левая часть функциональной зависимости) не является ключом, но зависимая часть входит в состав какого-либо потенциального ключа. В НФБК такое запрещено.

Проще говоря, НФБК требует, чтобы любая зависимость шла только от ключа.

Пример нарушения НФБК

Добавим в таблицу телефоны городов, чтобы получить зависимость Город → Телефон_города.

 «Поставщики»

ID_Поставщика

Поставщик

Город

Телефон_города

1

ООО “Ромашка”

Москва

+7-495-000-01

2

ООО “Букашка”

Санкт-Петербург

+7-812-000-01

3

ООО “Цветочек”

Казань

+7-843-000-01

4

ООО “Ягодка”

Москва

+7-495-000-01 

Поскольку город не суперключ (Москва встречается несколько раз) и при этом он определяет другой атрибут, то эта зависимость нарушает НФБК. Исправляем данную проблему выносом телефон города в отдельную таблицу

«Города»

Город

Телефон_города

Москва

+7-495-000-01

Санкт-Петербург

+7-812-000-01

Казань

+7-843-000-01 

«Поставщики»

ID_Поставщика

Поставщик

Город

1

ООО “Ромашка”

Москва

2

ООО “Букашка”

Санкт-Петербург

3

ООО “Цветочек”

Казань

4

ООО “Ягодка”

Москва

Теперь наша зависимость Город → Телефон_города вынесена отдельно. В каждой таблице все зависимости идут от ключей. Достигнута Нормальная форма Бойса-Кодда.

Дальнейшее использование нормализации не является целесообразным решением ввиду снижения производительности и в значительном увеличении количества таблиц. Хорошая база данных – это база, которая достаточно нормализована, чтобы не создавать аномалий для пользователя базы данных, и в то же время обладает прекрасной производительностью.

На практике чаще всего используется третья нормальная форма (3НФ).

Благодарю за прочтение!

Комментарии (9)


  1. Akina
    23.03.2026 10:17

    Аномалии обновления 

    Это - не аномалия. Это разрушение данных как следствие кривых рук. Кого именно - программиста или оператора,- дело десятое.

    Аномалия удаления

    А это вообще бред сивой кобылы. Что значит "исчезает информация о существовании курса С++ и платформы Б". Вы забыли, что до ввода этой записи в БД тоже никакой информации об этих курсе и платформе не было? Однако это как-то не помешало эту запись ввести.

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

    Все получилось в точности как в исходной таблице – нет лишних строк и потерь данных.

    Случайность, невозможная на практике. Просто везение, что каждый предмет ведёт строго один преподаватель.

    -------------------

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


    1. buffalo_buffalo_buffalo
      23.03.2026 10:17

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

      про ограничение целостности что-нибудь слышали?


      1. Akina
        23.03.2026 10:17

        А при чём тут ограничение целостности? Автор рассматривает ТОЛЬКО ДАННЫЕ, без какой-либо корреляции со структурой их хранения, которой ещё просто нет.

        Что же до ограничения целостности в распределении преподавателей по преподаваемым предметам - не то что не слышал, но даже и представить себе такое не могу. Особенно на практике.

        И да - про ограничения целостности в базах данных слышал, и не раз.


  1. Ka1seR Автор
    23.03.2026 10:17

    Это - не аномалия. Это разрушение данных как следствие кривых рук.

    Не совсем понял что вы хотели этим сказать. Это существующий термин, используемый тем же Дейтом. Могу лишь согласиться, что на примере с одной таблицей(как в статье), у новичков могут возникнуть вопросы по типу: «А что плохого? Мы ведь можем пройтись по всем строкам и заменить значения». Можем конечно, только сколько времени и ресурсов это займет? А если таблица имеет миллион строк? Суть то тут в том, что в НОРМАЛИЗОВАННОЙ БД обновление данных должно быть выполнено одним действием. Если требуется поменять "Платформа А" на "Платформа В» во всей базе - это должно делаться одним действием. Тык - и поменялось.

    Что значит "исчезает информация о существовании курса С++ и платформы Б"

    Когда мы удаляем информацию о студенте Петре, мы должны удалить ТОЛЬКО информацию относящуюся непосредственно к Петру. А мы удалил еще и сведения о существовании у нас курса C++. Попробуйте найти ответ на вопрос «Сколько различных курсов преподают в университете?» до и после удаления Пети. Спойлер: ответы будут отличаться, хотя курс никуда не должен был деться, ведь его мы вообще не хотели трогать.

    Случайность, невозможная на практике. Просто везение, что каждый предмет ведёт строго один преподаватель.

    Когда это прочел, каюсь, не сдержал смех. Вы ко всем обучающим примерам так относитесь? Вы учебники хоть раз в жизни открывали? Много ли там трушных примеров? Да и зачем они здесь, если суть в том, что после JOIN таблиц при неправильной декомпозиции мы терям исходные данные(появление несуществующих до декомпозиции строк я тоже отношу к потерям данных) Вот неужели для такого невероятного mind-blowing факта вам не хватило простого примера?


    1. Akina
      23.03.2026 10:17

      А мы удалил еще и сведения о существовании у нас курса C++.

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

      Попробуйте найти ответ на вопрос «Сколько различных курсов преподают в университете?»

      Более того, вы не можете получить гарантированно правильный ответ на этот вопрос и без удаления этой записи - потому что нет никакой гарантии, что не существует предметов, которые на текущий момент никому не преподают.

      Вы учебники хоть раз в жизни открывали? Много ли там трушных примеров?

      Изрядное количество задач в учебниках вообще слабо соотносится с действительностью - это для меня не новость. Но почему нужно ориентироваться на худшие практики?

      суть в том, что после JOIN таблиц при неправильной декомпозиции мы терям исходные данные

      Я вам ещё в комментарии к предыдущей статье указывал, что нельзя создавать структуру только на основании данных В основе схемы БД должна лежать "физика" предметной области. А данные просто позволяют проверить те или иные предположения - в том числе и обратной реконструкцией исходного ненормализованного вида данных.


  1. vadpost
    23.03.2026 10:17

    Когда впервые узнал про нормальные формы (мне было достаточно трёх), я почувствовал себя полубогом всех данных, каюсь )


  1. Ivan22
    23.03.2026 10:17

    Честно говоря статья на википедии понятнее


  1. killyself
    23.03.2026 10:17

    Структура базы в конфигурациях 1С частенько соответствует 4НФ. Даже иногда пятой, но не в проде.


    1. Ivan22
      23.03.2026 10:17

      соболезную