Learning Platform
Глоссарий Troubleshooting
Урок 08.05 · 18 мин
Начальный
3nftransitive-dependencynormalizationdata-integrity

3NF: устранение transitive dependencies

Таблица в 2NF: она в 1NF и в ней нет partial dependencies — ни один non-prime атрибут не зависит от части ключа. Но 2NF закрывает только один вид «неправильной» зависимости. Остаётся ещё один: non-prime атрибут может зависеть от другого non-prime атрибута. Такой дефект 2NF не замечает — а избыточность и аномалии он порождает ровно те же.

Третья нормальная форма (3NF) устраняет этот последний из «школьных» дефектов — transitive dependency (транзитивную зависимость). 3NF — это практический рабочий стандарт для OLTP-схем: подавляющее большинство нормализованных баз приложений доводят именно до 3NF. В этом уроке мы дадим определение 3NF, разберём транзитивную зависимость и пройдём декомпозицию из 2NF в 3NF на конкретной таблице.


Что такое transitive dependency

Transitive dependency (транзитивная зависимость) — это зависимость non-prime атрибута от другого non-prime атрибута. Название — от аксиомы транзитивности Армстронга: если ключ -> A и A -> B, то по транзитивности ключ -> B, но при этом B достаётся ключу «через посредника» A, а не напрямую.

Рассмотрим таблицу сотрудников. Первичный ключ — emp_id, одноатомный, значит таблица заведомо в 2NF (partial dependency невозможна):

EMPLOYEES
emp_id PK | emp_name | dept_id | dept_name   | dept_location
----------+----------+---------+-------------+--------------
1         | Анна     | D-1     | Продажи     | Москва
2         | Иван     | D-1     | Продажи     | Москва
3         | Пётр     | D-2     | Логистика   | Казань
4         | Мария    | D-1     | Продажи     | Москва

Выпишем FD:

emp_id  -> emp_name
emp_id  -> dept_id
dept_id -> dept_name
dept_id -> dept_location

Атрибуты dept_name и dept_location зависят от dept_id. Но dept_idnon-prime атрибут (в ключ emp_id он не входит). Получается цепочка:

emp_id -> dept_id -> dept_name
(ключ)    (non-prime)  (non-prime)

dept_name зависит от ключа emp_id транзитивно — через посредника dept_id. Это и есть transitive dependency. Формально: non-prime атрибут dept_name зависит от non-prime атрибута dept_id.

Транзитивная зависимость: атрибут достаётся ключу через посредника
Ключemp_id — первичный ключ таблицы
определяет
Посредникdept_id — non-prime атрибут, сам не ключ
определяет
Жертваdept_name зависит от ключа лишь транзитивно, через non-prime dept_id

Partial dependency здесь нет (ключ одноатомный), поэтому 2NF выполняется. Но транзитивная зависимость делает своё дело. Так как dept_name и dept_location зависят только от dept_id, они одинаковы во всех строках сотрудников одного отдела. Отдел D-1 «Продажи / Москва» в нашем примере продублирован трижды. Полный набор аномалий: переименование отдела — UPDATE множества строк сотрудников (риск рассинхрона); новый отдел без сотрудников не завести (insert anomaly — нет emp_id); увольнение последнего сотрудника отдела стирает сам отдел (delete anomaly).


Определение 3NF

Есть две эквивалентные формулировки 3NF. Полезно знать обе.

Формулировка 1 (через транзитивные зависимости). Таблица в 3NF, если она в 2NF и не содержит транзитивных зависимостей — то есть ни один non-prime атрибут не зависит от другого non-prime атрибута. Каждый non-prime атрибут зависит от ключа напрямую, «не через посредника».

Формулировка 2 (общий критерий). Таблица в 3NF, если для каждой нетривиальной функциональной зависимости X -> A выполнено хотя бы одно из двух условий:

  • X является superkey, либо
  • A является prime attribute (входит в какой-нибудь candidate key).

Вторая формулировка строже и удобнее для проверки. Прогоним по ней нашу таблицу. FD dept_id -> dept_name: является ли dept_id superkey? Нет — {dept_id}+ = {dept_id, dept_name, dept_location}, не все атрибуты. Является ли dept_name prime-атрибутом? Нет — единственный candidate key это emp_id, dept_name в него не входит. Оба условия нарушены — значит таблица не в 3NF.

NOTE

Вторая формулировка содержит послабление «или A — prime attribute». Именно это послабление отличает 3NF от более строгой BCNF. 3NF разрешает зависимость X -> A, где X не superkey, при условии что A — prime. BCNF этого послабления не даёт. Разница между 3NF и BCNF и её практические последствия — тема следующего модуля; пока достаточно знать, что 3NF в этом месте чуть мягче.


Декомпозиция в 3NF: пошагово

Приведём EMPLOYEES к 3NF. Принцип: вынести атрибуты, зависящие от non-prime «посредника», в отдельную таблицу, где посредник станет первичным ключом.

Шаг 1. Найти candidate keys. {emp_id}+ = все атрибуты — candidate key. Других нет. Единственный candidate key — emp_id; prime-атрибут один — emp_id.

Шаг 2. Выписать FD и найти транзитивные. emp_id -> emp_name — напрямую от ключа, в порядке. emp_id -> dept_id — напрямую от ключа, в порядке. dept_id -> dept_name и dept_id -> dept_location — детерминант dept_id не superkey, правые части не prime — это транзитивные зависимости, дефект.

Шаг 3. Вынести зависимые от посредника атрибуты в свою таблицу. Атрибуты, зависящие от dept_id, переезжают в DEPARTMENTS, где dept_id становится первичным ключом:

DEPARTMENTS (3NF)
dept_id PK | dept_name  | dept_location
-----------+------------+--------------
D-1        | Продажи    | Москва
D-2        | Логистика  | Казань

Шаг 4. В исходной таблице оставить ключ и атрибуты прямой зависимости. В EMPLOYEES остаётся emp_name и dept_id; dept_id превращается в foreign key на DEPARTMENTS:

EMPLOYEES (3NF)
emp_id PK | emp_name | dept_id FK
----------+----------+-----------
1         | Анна     | D-1
2         | Иван     | D-1
3         | Пётр     | D-2
4         | Мария    | D-1
CREATE TABLE departments (
    dept_id       TEXT PRIMARY KEY,
    dept_name     TEXT NOT NULL,
    dept_location TEXT NOT NULL
);
CREATE TABLE employees (
    emp_id   INT  PRIMARY KEY,
    emp_name TEXT NOT NULL,
    dept_id  TEXT NOT NULL REFERENCES departments(dept_id)
);
Декомпозиция в 3NF: посредник становится ключом своей таблицы
2NFEMPLOYEES со столбцами dept_name, dept_location — транзитивные зависимости через dept_id
вынести по dept_id
DEPARTMENTSdept_id PK, dept_name, dept_location — то, что зависело от посредника
EMPLOYEESemp_id PK, emp_name, dept_id FK — только прямые зависимости от ключа

Шаг 5. Проверить результат. DEPARTMENTS: единственный candidate key dept_id, FD dept_id -> dept_name имеет superkey слева — 3NF. EMPLOYEES: FD emp_id -> emp_name и emp_id -> dept_id имеют superkey emp_id слева, транзитивных зависимостей нет — 3NF.

Аномалии исчезли. Переименование отдела — UPDATE одной строки DEPARTMENTS. Новый отдел — INSERT INTO departments без сотрудников. Увольнение последнего сотрудника отдела трогает только EMPLOYEES, строка отдела цела. Декомпозиция lossless: общий атрибут dept_id — первичный ключ DEPARTMENTS, соединение по нему точно восстановит исходную таблицу.


Почему транзитивная зависимость опаснее, чем кажется

Транзитивную зависимость легко недооценить: «ну продублировано название отдела, не страшно». Покажем на цифрах, почему страшно — и почему 3NF на практике обязательна.

Избыточность от транзитивной зависимости масштабируется с размером дочерней группы. Если в отделе D-1 работает 500 сотрудников, то строка «D-1 / Продажи / Москва» физически записана 500 раз. Переименование отдела — это UPDATE 500 строк. И каждая такая строка — отдельная цель для рассинхрона: достаточно одной незавершённой транзакции, одного кривого WHERE, одной гонки — и часть сотрудников «работает в Продажах», а часть в старом названии. На вопрос «как называется отдел D-1» база начинает давать два ответа.

Сравним стоимость одной и той же операции «переименовать отдел» в двух схемах:

              | строк под UPDATE | риск рассинхрона | новый отдел без людей
--------------+------------------+------------------+----------------------
до 3NF        | N (= число       | высокий, растёт  | невозможно
(EMPLOYEES    |   сотрудников     | с N              | (insert anomaly)
 с dept_*)    |   отдела)        |                  |
--------------+------------------+------------------+----------------------
после 3NF     | ровно 1          | нулевой          | INSERT в departments
(DEPARTMENTS) | (одна строка)    | (один факт —     | без сотрудников
              |                  |  одна ячейка)    |

Разница не количественная, а качественная. До 3NF корректность операции зависит от того, безошибочно ли отработал код на всех N строках. После 3NF корректность гарантирована структурой: факт хранится в одной ячейке, рассинхронизировать его физически нечем. Транзитивная зависимость — это не «немного лишних байт», это перенос ответственности за инвариант с СУБД на надежду.

Поэтому 3NF — не «уровень для перфекционистов», а практический минимум для любой OLTP-схемы, которую правят в реальном времени. Остановиться раньше 3NF — значит сознательно оставить в базе незащищённые инварианты.

Нормализация в PostgreSQL: практика 1NF, 2NF, 3NF
WARNING

Чем больше дочерняя группа (сотрудников в отделе, товаров у поставщика, заказов у города), тем дороже обходится транзитивная зависимость: больше строк под каждым обновлением, выше вероятность рассинхрона. На больших таблицах нарушение 3NF превращает рутинную правку справочного значения в рискованную операцию.

Worked example: вся цепочка 1NF -> 2NF -> 3NF

Соберём весь модуль в одном примере. Денормализованная таблица:

ORDER_REPORT (нарушает 1NF)
order_id | items                 | customer_id | customer_name | city_code | city_name
---------+-----------------------+-------------+---------------+-----------+----------
1001     | P-7:2, P-9:1          | 42          | Анна          | C-MOW     | Москва

FD: order_id -> customer_id, customer_id -> customer_name, customer_id -> city_code, city_code -> city_name, {order_id, product_id} -> qty, product_id -> product_name.

В 1NF — развернём список товаров в строки, заведём составной ключ {order_id, product_id}:

ORDER_REPORT_1NF(order_id, product_id, qty, product_name, customer_id, customer_name, city_code, city_name)
PK = {order_id, product_id}

В 2NF — уберём partial dependencies. От части ключа product_id зависит product_name; от части ключа order_id зависят customer_id, customer_name, city_code, city_name (через order_id -> customer_id -> ...):

PRODUCTS(product_id PK, product_name)
ORDERS(order_id PK, customer_id, customer_name, city_code, city_name)
ORDER_ITEMS(order_id FK, product_id FK, qty)   PK = {order_id, product_id}

В 3NF — в таблице ORDERS остались транзитивные зависимости: order_id -> customer_id -> customer_name и далее customer_id -> city_code -> city_name. Цепочку разворачиваем по посредникам customer_id и city_code:

CITIES(city_code PK, city_name)
CUSTOMERS(customer_id PK, customer_name, city_code FK)
ORDERS(order_id PK, customer_id FK)
PRODUCTS(product_id PK, product_name)
ORDER_ITEMS(order_id FK, product_id FK, qty)   PK = {order_id, product_id}

Пять таблиц, каждая в 3NF. Каждый факт хранится ровно один раз: название города — в CITIES, имя покупателя — в CUSTOMERS, название товара — в PRODUCTS. Все insert/update/delete-аномалии устранены.

TIP

3NF — практический стандарт для OLTP. Доводить до неё стоит почти всегда: она убирает избыточность и аномалии, оставаясь интуитивно понятной и не плодя лишних таблиц. BCNF, 4NF, 5NF из следующего модуля закрывают более редкие крайние случаи. Если запомнить одно правило про нормализацию для приложений — это «доводи схему до 3NF».


Попробуй сам

Дана таблица в 2NF (ключ одноатомный):

BOOKS
book_id PK | title | publisher_id | publisher_name | publisher_country

FD: book_id -> title, book_id -> publisher_id, publisher_id -> publisher_name, publisher_id -> publisher_country.

Выполните на бумаге:

  1. Найдите транзитивную зависимость. Назовите посредника и объясните, почему этот атрибут non-prime.
  2. Проверьте таблицу по второй формулировке 3NF (X — superkey или A — prime) для FD publisher_id -> publisher_name. Какое из двух условий нарушено?
  3. Приведите таблицу к 3NF. Укажите PK и FK получившихся таблиц.
  4. Покажите на конкретной операции delete anomaly, который существовал до декомпозиции и исчез после.

Проверка знанийKnowledge check
Что такое transitive dependency, как её определяет 3NF через две эквивалентные формулировки, и почему 2NF не способна устранить этот дефект?
ОтветAnswer
Transitive dependency (транзитивная зависимость) — это зависимость non-prime атрибута от другого non-prime атрибута: возникает цепочка ключ -> A -> B, где A и B оба не входят в candidate key, и атрибут B достаётся ключу не напрямую, а через посредника A. 3NF определяется двумя эквивалентными способами. Первый: таблица в 3NF, если она в 2NF и не содержит транзитивных зависимостей — каждый non-prime атрибут зависит от ключа напрямую. Второй (общий критерий): таблица в 3NF, если для каждой нетривиальной FD X -> A выполнено X является superkey ИЛИ A является prime attribute. 2NF не способна устранить транзитивную зависимость, потому что 2NF проверяет только один вид дефекта — зависимость non-prime атрибута от ЧАСТИ составного ключа (partial dependency). Транзитивная зависимость — это зависимость non-prime атрибута от другого non-prime атрибута, а не от части ключа; формально 2NF её нарушением не считает. Более того, при одноатрибутном первичном ключе таблица автоматически в 2NF, но всё ещё может содержать транзитивные зависимости и порождать те же избыточность и аномалии. Поэтому нужна отдельная нормальная форма — 3NF, которая выносит атрибуты, зависящие от non-prime посредника, в отдельную таблицу, где посредник становится первичным ключом.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что такое transitive dependency в контексте 3NF?

Закончили урок?

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

Войдите чтобы оценить урок

Прогресс модуля
0 из 5