SQL и Qt

Нормализация: 1NF, 2NF, 3NF — проектирование без избыточности

Введение

В заключительной статье первого блока цикла разберём нормализацию — формальный процесс проектирования структуры таблиц, минимизирующий избыточность хранимых данных и связанные с ней риски противоречивости при модификации, через последовательность нормальных форм (1NF, 2NF, 3NF — наиболее практически значимые из существующего более длинного формального списка).

Концепция

Первая нормальная форма (1NF) требует атомарности значений каждого столбца (никаких списков значений в одном столбце, упомянутых как подводный камень в статье 393) и отсутствия повторяющихся групп столбцов. Вторая нормальная форма (2NF) требует, чтобы каждый неключевой столбец зависел от ВСЕГО составного первичного ключа целиком, а не только от его части (актуально для таблиц с составным, многостолбцовым первичным ключом). Третья нормальная форма (3NF) требует отсутствия транзитивных зависимостей — неключевой столбец не должен зависеть от другого неключевого столбца (только напрямую от первичного ключа).

Пример кода: Нормализация

-- Нарушение 1NF — список значений в одном столбце
CREATE TABLE bad_orders (
    id INTEGER PRIMARY KEY,
    customer_name VARCHAR(100),
    product_names VARCHAR(500) -- "Молоко, Хлеб, Яйца" — нарушение атомарности
);

-- Исправление через 1NF — отдельная таблица для позиций заказа
CREATE TABLE orders_1nf (
    id INTEGER PRIMARY KEY,
    customer_name VARCHAR(100)
);
CREATE TABLE order_items_1nf (
    order_id INTEGER REFERENCES orders_1nf(id),
    product_name VARCHAR(100)
);
-- Нарушение 2NF — составной ключ (order_id, product_id), но product_name зависит
-- только от product_id, а не от полного составного ключа
CREATE TABLE bad_order_items (
    order_id INTEGER,
    product_id INTEGER,
    product_name VARCHAR(100), -- зависит ТОЛЬКО от product_id — нарушение 2NF
    quantity INTEGER,
    PRIMARY KEY (order_id, product_id)
);

-- Исправление через 2NF — product_name выносится в отдельную таблицу products
CREATE TABLE products_2nf (
    product_id INTEGER PRIMARY KEY,
    product_name VARCHAR(100)
);
CREATE TABLE order_items_2nf (
    order_id INTEGER,
    product_id INTEGER REFERENCES products_2nf(product_id),
    quantity INTEGER,
    PRIMARY KEY (order_id, product_id)
);
-- Нарушение 3NF — customer_city зависит от customer_id (неключевого столбца), а не от order id
CREATE TABLE bad_orders_3nf (
    id INTEGER PRIMARY KEY,
    customer_id INTEGER,
    customer_city VARCHAR(100) -- транзитивная зависимость: id -> customer_id -> customer_city
);

-- Исправление через 3NF — город клиента хранится только в таблице клиентов
CREATE TABLE customers_3nf (
    id INTEGER PRIMARY KEY,
    city VARCHAR(100)
);
CREATE TABLE orders_3nf (
    id INTEGER PRIMARY KEY,
    customer_id INTEGER REFERENCES customers_3nf(id) -- город получаем через JOIN, не дублируем
);

Пояснения к коду

Исправление нарушения 1NF разделением списка товаров на отдельную таблицу order_items_1nf — прямое применение требования атомарности: каждая строка теперь содержит ровно одно значение product_name на одну позицию заказа, а не неструктурированный, требующий парсинга список. Исправление 2NF выносом product_name в отдельную таблицу products_2nf устраняет дублирование — без этого исправления имя одного и того же товара повторялось бы во множестве строк bad_order_items для каждого заказа, в котором он встречается, и изменение названия товара требовало бы обновления множества строк вместо одной. Исправление 3NF аналогично устраняет дублирование customer_city во множестве заказов одного клиента — город хранится один раз в таблице клиентов и получается через JOIN при необходимости, а не дублируется в каждой строке заказа.

Подводные камни

  • Доведение нормализации до крайности без учёта практических соображений производительности — полная, строгая нормализация минимизирует избыточность, но может требовать большего количества JOIN для получения практически нужных данных, что для часто выполняемых, критичных по производительности запросов иногда оправдывает осознанную денормализацию (статьи 403, 420) — нормализация — инструмент для минимизации рисков противоречивости данных, а не догма, применяемая без учёта реальных требований конкретной системы.
  • Путаница между формальными определениями нормальных форм и практическим, интуитивным пониманием «что куда вынести» — хотя формальные определения (через понятия функциональной зависимости) точны, на практике большинство разработчиков успешно нормализуют схему через более интуитивный вопрос «если я изменю одно значение, должен ли я не забыть изменить его и где-то ещё?» — если ответ «да», это обычно сигнал об избыточности, которую стоит устранить через дополнительную нормализацию.
  • Применение нормализации только на начальном этапе проектирования без пересмотра по мере роста и изменения требований системы — структура, нормализованная правильно для изначальных требований, может перестать быть оптимальной по мере добавления новых, не предусмотренных изначально функций приложения, и периодический пересмотр схемы БД (с соответствующими миграциями, статья 428) — нормальная часть долгосрочного сопровождения системы, а не разовое действие, выполняемое только при первоначальном проектировании.
  • Нормализация данных, которые по своей природе редко изменяются и для которых дублирование не создаёт реального риска противоречивости (например, исторические «снимки» данных на момент совершения транзакции — сохранённое имя товара и его цена на момент покупки, которые намеренно НЕ должны автоматически обновляться, если реальное название/цена товара изменится позже) — для таких случаев намеренное «дублирование» данных в момент создания записи — не нарушение принципов нормализации в смысле риска противоречивости, а осознанный, корректный дизайн исторической, неизменяемой записи о состоянии данных на конкретный момент времени.

Первый блок цикла «Углублённый цикл SQL» (статьи 393-402, основы и теория реляционных БД) завершён. Далее следует блок 2 — продвинутые запросы и проектирование (403-412).