Хорошая схема делает неправильные данные невозможными, а частые запросы простыми.
Ключи
- Первичный ключ (PK) однозначно идентифицирует строку: уникален и не NULL. Обычно суррогатный:
id bigint GENERATED ALWAYS AS IDENTITYили UUID. - Естественный ключ берётся из предметной области (ИНН, email). Он может меняться, поэтому его чаще делают
UNIQUE, а PK оставляют суррогатным. - UUID против bigint: UUID можно генерировать на клиенте и в разных сервисах, его не угадать перебором. Но он в 2 раза длиннее, а случайный UUID v4 вставляется в случайное место B-tree и фрагментирует индекс. UUID v7 упорядочен по времени и этой проблемы почти лишён.
- Внешний ключ (FK) гарантирует, что ссылка указывает на существующую строку.
ON DELETE CASCADEудалит детей вместе с родителем,RESTRICTзапретит удаление,SET NULLобнулит ссылку.
Связи
- Один к одному: FK с ограничением UNIQUE (профиль пользователя).
- Один ко многим: FK на стороне «многих» (
orders.customer_id). - Многие ко многим: отдельная связующая таблица, например
student_courses(student_id, course_id)с составным PK.
Нормализация
Цель: каждый факт хранится в одном месте, чтобы не было аномалий обновления (поменяли адрес клиента в одном заказе, а в другом остался старый).
- 1НФ: значения атомарны, нет повторяющихся групп. Колонка
phones = '111, 222'нарушает 1НФ, телефоны выносят в отдельную таблицу. - 2НФ: 1НФ и каждая неключевая колонка зависит от всего составного ключа. В
order_items(order_id, product_id, qty, product_title)название товара зависит только отproduct_id: нарушение. - 3НФ: 2НФ и нет транзитивных зависимостей: неключевая колонка не зависит от другой неключевой.
employees(id, department_id, department_name): название отдела зависит отdepartment_id, а не от сотрудника. - BCNF: строже 3НФ, каждый детерминант является потенциальным ключом. На собесе обычно достаточно понимать 1-3НФ.
Денормализация
Иногда данные дублируют сознательно: ради скорости чтения (счётчик comments_count у поста), ради истории (цена товара на момент заказа хранится в order_items.price, это не дубль, а другой факт). Денормализация требует механизма синхронизации: триггер, код приложения или фоновая пересборка.
Ограничения как защита
NOT NULLтам, где значение обязательно.CHECK (qty > 0),CHECK (ends_at > starts_at).UNIQUE (user_id, slug).- Частичный уникальный индекс:
CREATE UNIQUE INDEX ON users (email) WHERE deleted_at IS NULLразрешает повторно зарегистрировать удалённый email.
Проверка только в коде приложения не спасает от гонок: два параллельных запроса оба проверят «email свободен» и оба вставят. Ограничение в базе не даст.
Типы
- Деньги:
numeric(12,2)или целые копейки вbigint. Неfloat: 0.1 + 0.2 не равно 0.3. - Время:
timestamptz, хранит момент в UTC и показывает в зоне сессии. jsonbдля действительно гибких атрибутов, а не вместо всей схемы.textв PostgreSQL не медленнееvarchar(n).
Частые ошибки
- Список значений через запятую в одной колонке.
- Деньги во float.
timestampбез зоны для событий.- Уникальность только в коде приложения.
- FK без индекса на дочерней стороне.
Что спросят на собесе
- Что такое нормальные формы, пример нарушения 2НФ и 3НФ.
- Когда денормализация оправдана.
- Как спроектировать связь многие ко многим.
- UUID или автоинкремент: плюсы и минусы.
- Как хранить деньги и время.