Нормализация данных: 1НФ, 2НФ, 3НФ
Представьте таблицу университета, где в одной ячейке через запятую перечислены все курсы студента, а рядом смешаны телефон и адрес. Если человек сменит фамилию или переедет, эту цепочку придётся искать и править во множестве мест. Нормализация — это осознанное разбиение данных на таблицы и связи, чтобы убрать дубли, предотвратить противоречия и упростить сопровождение базы.
Правило 1НФ простое: в каждой ячейке — одно неделимое значение из домена. Никаких «списков через запятую» в одном поле: для этого есть отдельные строки или отдельная таблица.
-- Плохо: нарушение 1НФ — несколько телефонов в одной ячейке
CREATE TABLE students_bad (
id SERIAL PRIMARY KEY,
full_name TEXT NOT NULL,
phones TEXT -- '+7999..., +7911...'
);
-- Хорошо: 1НФ — один телефон на строку
CREATE TABLE students (
id SERIAL PRIMARY KEY,
full_name TEXT NOT NULL
);
CREATE TABLE student_phones (
id SERIAL PRIMARY KEY,
student_id INT NOT NULL REFERENCES students(id),
phone TEXT NOT NULL
);
2НФ актуальна при составном первичном ключе. Каждый неключевой атрибут должен зависеть от всего ключа целиком, а не от его части. Если ключ — пара «студент + курс», то длительность курса зависит только от курса: хранить её в строке зачисления значит копировать одно и то же и рисковать рассинхроном.
-- Плохо: частичная зависимость от составного ключа
CREATE TABLE enrollments_bad (
student_id INT NOT NULL,
course_id INT NOT NULL,
enrolled_at DATE NOT NULL,
course_duration_hours INT NOT NULL, -- дубли на каждого студента
PRIMARY KEY (student_id, course_id)
);
-- Хорошо: длительность хранится один раз, в каталоге курсов
CREATE TABLE courses (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
duration_hours INT NOT NULL
);
CREATE TABLE enrollments (
student_id INT NOT NULL,
course_id INT NOT NULL REFERENCES courses(id),
enrolled_at DATE NOT NULL,
PRIMARY KEY (student_id, course_id)
);
3НФ запрещает транзитивные зависимости: неключевой столбец не должен зависеть от другого неключевого столбца. Если у студента есть `city_id` и рядом дублируется `city_name`, имя города зависит от идентификатора города, а не от первичного ключа студента. Имя нужно хранить в отдельном справочнике.
-- Плохо: city_name транзитивно зависит от city_id, а не от id студента
CREATE TABLE students_bad_city (
id SERIAL PRIMARY KEY,
full_name TEXT NOT NULL,
city_id INT NOT NULL,
city_name TEXT NOT NULL -- дублируем справочник
);
-- Хорошо: справочник городов, у студента только city_id
CREATE TABLE cities (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE students (
id SERIAL PRIMARY KEY,
full_name TEXT NOT NULL,
city_id INT NOT NULL REFERENCES cities(id)
);