Раздел 4 — Нормализация и целостность данных

Как убрать избыточность из схемы и заставить базу самостоятельно защищать данные

Прогресс курса Раздел 4 из 13

Что вы освоите в этом разделе

6 академических часов: 2 часа теории, 4 часа практики. Практика продолжает работу над той предметной областью, которую вы выбрали в разделе 3.
01

Аномалии: зачем вообще нормализовать

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

Возьмём таблицу поставок, в которую свалили всё сразу:

sup_idsup_namecitycity_statusprod_idprod_nameqty
1ТехноСнабМосква10101Болт М8500
1ТехноСнабМосква10102Гайка М8800
2МеталлТоргКазань20101Болт М8300
3СтальПромМосква10103Шайба1200

Первичный ключ здесь — пара (sup_id, prod_id): один поставщик поставляет один товар в определённом количестве. Из этого рождаются три беды.

Три вида аномалий

  • Аномалия вставки. Появился новый поставщик, но он ещё ничего не поставил. Записать его некуда: без prod_id строка не вставится — это часть первичного ключа. Информация о поставщике не может существовать без поставки, хотя в реальности вполне существует.
  • Аномалия обновления. «ТехноСнаб» переехал в Казань. Обновить нужно все строки этого поставщика. Обновили не все — база утверждает, что один и тот же поставщик находится в двух городах одновременно. Данные противоречат сами себе.
  • Аномалия удаления. «МеталлТорг» перестал поставлять болты — удаляем строку. Вместе с ней исчезает вся информация о поставщике: название, город, статус. Мы потеряли данные, которые удалять не собирались.
Корень всех трёх аномалий один: в одном отношении смешаны факты о разных сущностях — о поставщике, о товаре и о поставке. Нормализация — это разделение таких фактов по отдельным отношениям.
02

Функциональные зависимости

Формальный инструмент нормализации — функциональная зависимость.

Определение. Атрибут B функционально зависит от атрибута A (записывается A → B), если каждому значению A соответствует ровно одно значение B. Читается: «A определяет B». Атрибут слева называют детерминантом.

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

Выпишем зависимости нашей таблицы:

sup_id            → sup_name, city, city_status
city              → city_status
prod_id           → prod_name
(sup_id, prod_id) → qty

Виды зависимостей

  • Полная функциональная зависимость — атрибут зависит от всего составного ключа и не зависит ни от какой его части. (sup_id, prod_id) → qty: количество определяется только парой.
  • Частичная зависимость — атрибут зависит от части составного ключа. sup_id → sup_name: имя поставщика определяется половиной ключа. Это нарушение 2НФ.
  • Транзитивная зависимость — атрибут зависит от ключа через промежуточный неключевой атрибут: sup_id → city → city_status. Это нарушение 3НФ.
  • Тривиальная зависимостьA → A или (A, B) → A. Выполняется всегда, интереса не представляет.
Практический приём: выпишите все функциональные зависимости до того, как начнёте делить таблицы. Дальше нормализация становится механической — каждая «лишняя» зависимость указывает, какую таблицу выделить, а её детерминант становится первичным ключом новой таблицы.
03

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

1НФ: все значения атомарны, нет повторяющихся групп, у отношения есть первичный ключ.

Это условие вы уже знаете из раздела 2 как свойство отношения. Нарушают его двумя способами.

Способ первый — список в ячейке:

idклиенттелефоны
1Иванов+7900…, +7911…

Способ второй — повторяющиеся группы столбцов:

idклиенттелефон_1телефон_2телефон_3
1Иванов+7900…+7911…NULL

Второй способ выглядит приличнее, но хуже: он вводит искусственное ограничение (три телефона максимум), большинство ячеек пустует, а поиск по всем телефонам требует перебора всех столбцов.

Решение в обоих случаях одно — вынести многозначный атрибут:

CREATE TABLE clients (
    id   integer      PRIMARY KEY,
    name varchar(100) NOT NULL
);

CREATE TABLE client_phones (
    client_id integer     NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
    phone     varchar(20) NOT NULL,
    PRIMARY KEY (client_id, phone)
);
Атомарность определяется предметной областью, а не типом данных. ФИО одной строкой атомарно, если по фамилии искать не нужно, и неатомарно, если нужно. Дата атомарна, хотя состоит из года, месяца и дня. Вопрос всегда один: собираетесь ли вы обращаться к части значения.
04

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

2НФ: отношение в 1НФ, и каждый неключевой атрибут полностью зависит от первичного ключа — то есть нет частичных зависимостей.

2НФ имеет смысл только при составном первичном ключе. Если ключ простой, частичных зависимостей быть не может, и отношение автоматически во 2НФ.

В нашей таблице поставок ключ составной — (sup_id, prod_id). Находим частичные зависимости:

sup_id  → sup_name, city, city_status   ← зависит от части ключа
prod_id → prod_name                     ← зависит от части ключа
(sup_id, prod_id) → qty                 ← полная зависимость, всё в порядке

Каждый детерминант становится ключом новой таблицы:

-- Факты о поставщике
CREATE TABLE suppliers (
    sup_id      integer      PRIMARY KEY,
    sup_name    varchar(100) NOT NULL,
    city        varchar(100) NOT NULL,
    city_status integer      NOT NULL   -- транзитивная зависимость, уберём в 3НФ
);

-- Факты о товаре
CREATE TABLE products (
    prod_id   integer      PRIMARY KEY,
    prod_name varchar(200) NOT NULL
);

-- Факт о поставке — то, что действительно зависит от пары
CREATE TABLE supplies (
    sup_id  integer NOT NULL REFERENCES suppliers(sup_id),
    prod_id integer NOT NULL REFERENCES products(prod_id),
    qty     integer NOT NULL CHECK (qty > 0),
    PRIMARY KEY (sup_id, prod_id)
);

Аномалия вставки исчезла: нового поставщика теперь можно записать в suppliers до первой поставки. Аномалия удаления тоже: удаление поставки не трогает данные о поставщике.

05

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

3НФ: отношение во 2НФ, и ни один неключевой атрибут не зависит транзитивно от первичного ключа — то есть неключевые атрибуты не зависят друг от друга.

В таблице suppliers осталась цепочка:

sup_id → city → city_status

Статус города определяется городом, а не поставщиком. Значит, статус продублирован у каждого поставщика из Москвы, и при его изменении придётся обновлять множество строк — аномалия обновления вернулась.

Выделяем город в справочник:

CREATE TABLE cities (
    city   varchar(100) PRIMARY KEY,
    status integer      NOT NULL
);

CREATE TABLE suppliers (
    sup_id   integer      PRIMARY KEY,
    sup_name varchar(100) NOT NULL,
    city     varchar(100) NOT NULL REFERENCES cities(city)
);

Теперь статус Москвы хранится ровно в одном месте.

Мнемоника Кодда для 3НФ: «каждый неключевой атрибут зависит от ключа, от всего ключа и ни от чего, кроме ключа». «От ключа» — это 1НФ, «от всего ключа» — 2НФ, «ни от чего, кроме ключа» — 3НФ.

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

ТаблицаО чём фактыКлюч
citiesо городеcity
suppliersо поставщикеsup_id
productsо товареprod_id
suppliesо поставке(sup_id, prod_id)
Для подавляющего большинства практических задач 3НФ достаточно. Дальнейшие формы решают более редкие ситуации, и на собеседовании обычно спрашивают именно про первые три.
06

Нормальная форма Бойса—Кодда

НФБК (BCNF): любой детерминант является потенциальным ключом. Более строгая версия 3НФ.

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

Правила предметной области:

Условия

  • Студент по одной дисциплине консультируется у одного преподавателя.
  • Каждый преподаватель ведёт ровно одну дисциплину.
  • Одну дисциплину ведут несколько преподавателей.

Отношение КОНСУЛЬТАЦИЯ(студент, дисциплина, преподаватель) имеет зависимости:

(студент, дисциплина) → преподаватель     ← потенциальный ключ
преподаватель         → дисциплина        ← детерминант, но НЕ ключ

Потенциальных ключей два: (студент, дисциплина) и (студент, преподаватель). Неключевых атрибутов нет вовсе — значит, отношение в 3НФ. Но в НФБК оно не находится: преподаватель является детерминантом, не будучи потенциальным ключом.

Последствия видны сразу: пока у преподавателя нет ни одного студента, факт «Петров ведёт базы данных» записать негде — аномалия вставки. Декомпозиция:

CREATE TABLE teachers (
    teacher   varchar(100) PRIMARY KEY,
    subject   varchar(100) NOT NULL
);

CREATE TABLE consultations (
    student varchar(100) NOT NULL,
    teacher varchar(100) NOT NULL REFERENCES teachers(teacher),
    PRIMARY KEY (student, teacher)
);
У декомпозиции есть цена: исходное правило «студент по одной дисциплине консультируется у одного преподавателя» теперь схемой не гарантируется — студент может попасть к двум преподавателям одной дисциплины. НФБК не всегда сохраняет функциональные зависимости, и иногда осознанно останавливаются на 3НФ, а правило проверяют триггером. Это тот случай, когда формальная чистота вступает в конфликт с полезностью.

Существуют также 4НФ (устраняет многозначные зависимости) и 5НФ (зависимости соединения). На практике встречаются редко; знать достаточно, что они есть и решают проблемы независимых многозначных фактов в одном отношении.

07

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

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

Когда денормализация оправдана

  • Историческое значение. Цена в позиции заказа дублирует цену товара, потому что должна остаться неизменной. Формально это нарушение, по сути — разные факты: «текущая цена» и «цена сделки».
  • Тяжёлый агрегат. Счётчик лайков у поста вместо COUNT по миллиону строк при каждом показе.
  • Аналитические витрины. В OLAP-хранилищах денормализация — норма, схема «звезда» намеренно избыточна.
  • Доказанное узкое место. Запрос действительно медленный, план выполнения изучен, индексы не помогли.
Порядок действий имеет значение: сначала нормализуйте, потом измеряйте, и только потом денормализуйте — точечно и с обоснованием. Денормализация «на всякий случай», до появления данных о производительности, даёт все аномалии сразу и никакого выигрыша. Каждое дублирование — это обязательство поддерживать копии в согласованном состоянии, обычно триггером.
08

Четыре вида целостности

Целостность данных — соответствие содержимого базы правилам предметной области. Различают четыре вида.

ВидЧто гарантируетЧем обеспечивается
Сущностнаякаждая строка уникально идентифицируется, ключ не NULLPRIMARY KEY
Ссылочнаяссылка ведёт на существующую строкуFOREIGN KEY
Доменнаязначение принадлежит допустимому множествутип данных, CHECK, NOT NULL, DOMAIN, ENUM
Пользовательскаявыполняются бизнес-правила предметной областиCHECK, UNIQUE, EXCLUDE, триггеры, код приложения
Правило приоритета: выражайте ограничение на самом низком уровне, где это возможно. Тип данных дешевле CHECK, CHECK дешевле триггера, триггер дешевле проверки в приложении. Чем ниже уровень, тем труднее правило обойти — а обходить его будут: скриптом миграции, импортом из Excel, руками через psql в пятницу вечером.
09

Ограничения в PostgreSQL

Соберём весь арсенал на одном примере — таблице бронирования переговорных комнат.

CREATE TABLE bookings (
    id         integer     GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    room_id    integer     NOT NULL REFERENCES rooms(id) ON DELETE RESTRICT,
    user_id    integer     NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    starts_at  timestamptz NOT NULL,
    ends_at    timestamptz NOT NULL,
    purpose    varchar(200),
    status     varchar(20)  NOT NULL DEFAULT 'active',

    -- доменная целостность: статус из списка
    CONSTRAINT bookings_status_valid
        CHECK (status IN ('active', 'cancelled', 'completed')),

    -- бизнес-правило: бронь не может кончиться раньше, чем началась
    CONSTRAINT bookings_period_valid
        CHECK (ends_at > starts_at),

    -- бизнес-правило: бронь не длиннее 8 часов
    CONSTRAINT bookings_max_duration
        CHECK (ends_at - starts_at <= interval '8 hours')
);

Отдельно стоит ограничение EXCLUDE — оно решает задачу, которую UNIQUE решить не может: запрет пересечения интервалов.

-- Требуется расширение для операторов над диапазонами
CREATE EXTENSION IF NOT EXISTS btree_gist;

-- Одна комната не может быть забронирована дважды на пересекающееся время
ALTER TABLE bookings ADD CONSTRAINT bookings_no_overlap
    EXCLUDE USING gist (
        room_id WITH =,
        tstzrange(starts_at, ends_at) WITH &&
    ) WHERE (status = 'active');
Читается так: запрещено существование двух строк, у которых совпадает room_id и пересекаются (&&) временные диапазоны — при условии, что бронь активна. Одно объявление заменяет триггер на два десятка строк и, в отличие от проверки в коде, работает корректно при параллельных вставках.

Что чем закрывается

  • NOT NULL — значение обязательно.
  • UNIQUE — нет повторов; работает и на нескольких столбцах.
  • CHECK — произвольное условие на строку; не может обращаться к другим таблицам.
  • PRIMARY KEYUNIQUE плюс NOT NULL, один на таблицу.
  • FOREIGN KEY — ссылочная целостность и правила каскада.
  • EXCLUDE — запрет сочетаний строк по произвольному оператору.
  • Триггер — когда правило затрагивает несколько таблиц или требует запроса. Подробно в приложении А1.
  • Частичный UNIQUE-индекс — уникальность при условии: CREATE UNIQUE INDEX … WHERE is_active.
Именуйте ограничения явно через CONSTRAINT имя. Автоматически сгенерированное имя вида bookings_check1 попадёт в текст ошибки, которую увидит пользователь приложения, и разбираться в ней придётся вам. Осмысленное имя bookings_period_valid экономит время при первом же инциденте.

Ключевые выводы раздела

Запомните главное

  • Нормализация устраняет три аномалии: вставки, обновления и удаления
  • Причина аномалий — факты о разных сущностях в одном отношении
  • Функциональные зависимости выводятся из смысла предметной области, а не из текущих данных
  • 1НФ — атомарность; 2НФ — нет частичных зависимостей; 3НФ — нет транзитивных
  • Мнемоника: «от ключа, от всего ключа и ни от чего, кроме ключа»
  • 2НФ имеет смысл только при составном ключе
  • НФБК строже 3НФ, но может не сохранять функциональные зависимости
  • Денормализация — только после измерений и только точечно
  • Четыре вида целостности: сущностная, ссылочная, доменная, пользовательская
  • Ограничение выражается на самом низком доступном уровне; EXCLUDE закрывает пересечения интервалов
Практические задания — на учебном портале. Задания, критерии оценивания, сдача работ и оценки преподавателя — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 3: Проектирование и ER Раздел 5: Установка PostgreSQL