SQL

БЛОК 1. Основы реляционных БД — 2. Проектирование таблиц

📚 24 вопросовПройти тест →
Лекция

БЛОК 1. Основы реляционных БД — 2. Проектирование таблиц

SQL

2. Проектирование таблиц

🧭 Введение: почему качество схемы важнее «красивого SQL»

Когда проект только начинается, таблицы часто делают «по ощущениям»: быстро добавляют поля и двигаются дальше.
На короткой дистанции это выглядит удобно, но уже через несколько недель появляются дубли, противоречия в данных и сложные баги в отчётах.
Проектирование таблиц нужно, чтобы заранее договориться о структуре данных: что именно хранится, как это связано и какие правила нельзя нарушать.
Хорошая схема снижает количество багов, ускоряет разработку и делает систему предсказуемой для всей команды.
💡 Совет: Проектируйте таблицы от бизнес-сценариев, а не от случайных полей, которые «сейчас нужны на экране».
Вывод: Проектирование таблиц — это фундамент, который определяет надёжность всей системы, а не формальная подготовка перед SQL.

⚠️ Проблема -> решение

Без проектирования команда обычно складывает «всё в одну таблицу», дублирует данные и связывает сущности по текстовым полям вроде email или названия курса.
В итоге любое изменение логики становится дорогим: нужно править много мест, а ошибки в данных уже накопились.
Решение — разделить данные на логические сущности, определить ключи и связи, а затем закрепить бизнес-правила ограничениями на уровне БД.
Тогда схема становится управляемой: данные согласованы, отчёты стабильны, а новые фичи добавляются без хаоса.
🟢 Если совсем просто: Сначала проектируем «скелет данных», потом пишем SQL.
🎯 Как понять, что этап прошёл успешно: Для каждой бизнес-сущности есть отдельная таблица, а связи и ограничения описаны явно.

🛠️ Чем помогает и как работает

Когда структура таблиц продумана заранее, команда тратит меньше времени на фиксы и ручные проверки данных.
Схема начинает работать как контракт: она подсказывает, что можно сохранить, а что нельзя.
🟢 Если совсем просто: Хорошая схема не даёт приложению «сломать» данные.
🎯 Как понять, что этап прошёл успешно: Ошибки целостности ловятся на уровне БД, а не в жалобах пользователей.
Чем помогает:
  • уменьшает дубли и расхождения между сервисами;
  • ускоряет написание запросов и аналитики;
  • упрощает подключение новых разработчиков;
  • делает миграции и развитие продукта безопаснее.
Как это работает:
  • Шаг 1: фиксируем бизнес-сущности (пользователь, курс, запись на курс, оплата).
  • Шаг 2: делаем минимальную нормализацию, чтобы один факт хранился в одном месте.
  • Шаг 3: определяем границы таблиц по правилу «одна таблица — одна логическая сущность».
  • Шаг 4: проектируем ключи (PRIMARY KEY, UNIQUE) для однозначной идентификации строк.
  • Шаг 5: описываем связи через FOREIGN KEY вместо ручных «текстовых» связок.
  • Шаг 6: задаём обязательность полей (NOT NULL) и базовые проверки (CHECK).
  • Шаг 7: планируем индексы под реальные сценарии чтения (WHERE, JOIN, ORDER BY).
  • Шаг 8: выносим связи N-N в отдельные таблицы-связки.
  • Шаг 9: проверяем схему на типовых сценариях чтения/изменения данных.
  • Шаг 10: только после этого пишем финальный DDL и вносим миграцию.
Вывод: Проектирование таблиц переводит разработку из режима «чинить последствия» в режим «предотвращать ошибки заранее».

🧼 Мини-нормализация: зачем убирать повторы

Без минимальной нормализации схема быстро превращается в набор дубликатов, где одно и то же значение хранится в нескольких местах.
Это создаёт системные ошибки: вы поправили данные в одной строке, но забыли в другой, и система стала противоречивой.
🟢 Если совсем просто: Нормализация нужна, чтобы каждый факт хранился в одном месте.
🎯 Как понять, что этап прошёл успешно: Изменение факта делается в одном месте, а не в пяти таблицах/строках сразу.
Мини-формула без перегруза:
  • убираем повторы атрибутов между сущностями;
  • храним каждый факт в одном месте;
  • связываем сущности ключами, а не копированием текста;
  • проверяем, что изменения и удаления не ломают другие данные.
Что ломается без этого (аномалии):
  • update anomaly — одно значение нужно менять в десятках строк, и часть остаётся старой;
  • insert anomaly — нельзя добавить новый факт без «лишних» данных;
  • delete anomaly — удаляя одну запись, случайно теряем важный факт.
Мини-пример:
Плохо: orders(order_id, user_email, user_name, course_title, course_price)Хорошо: users + courses + orders, связанные ключами
Вывод: Даже базовая нормализация резко снижает вероятность дубликатов и логических конфликтов в данных.

📚 Ключевые термины (простыми словами)

В этой теме важно говорить на одном языке внутри команды, иначе люди по-разному понимают «одно и то же».
Ниже короткий словарь, который нужно знать перед практикой.
🟢 Если совсем просто: Это словарь, чтобы не путаться в обсуждении схемы.
🎯 Как понять, что этап прошёл успешно: Вы можете объяснить каждый термин своими словами без заучивания формулировок.
  • Entity (сущность) — объект предметной области: пользователь, курс, оплата.
  • Attribute (атрибут) — характеристика сущности: email пользователя, цена курса.
  • Primary Key (первичный ключ) — уникальный идентификатор строки.
  • Foreign Key (внешний ключ) — ссылка на строку в другой таблице.
  • Cardinality (кардинальность связи) — тип связи: 1-1, 1-N, N-N.
  • Nullable (допускает NULL) — поле может быть пустым.
  • Unique Constraint (ограничение уникальности) — запрет дублирования значений.
  • Junction Table (таблица-связка) — отдельная таблица для связи N-N.
  • DDL (Data Definition Language) — SQL-команды для структуры БД (CREATE, ALTER).
Вывод: Без общего словаря команда спорит о терминах вместо проектирования.

🧱 1. Выделение сущностей и границ таблиц

Главная ошибка в начале проектирования — пытаться хранить всё в одной «универсальной» таблице.
Такой подход быстро ломается: данные становятся неоднозначными, а половина полей у каждой записи пустая.
🟢 Если совсем просто: Каждый тип бизнес-объекта должен жить в своей таблице.
🎯 Как понять, что этап прошёл успешно: По названию таблицы сразу понятно, какие данные в ней хранятся и зачем.
Назначение: Отделить разные бизнес-сущности друг от друга, чтобы схема была читаемой и расширяемой.
Простыми словами: Мы раскладываем данные «по коробкам»: пользователи отдельно, курсы отдельно, записи на курсы отдельно.
Для новичка: Если вы смотрите на строку таблицы и не понимаете, «это про что вообще», значит границы сущности выбраны плохо.
Аналогия: Это как склад: одежда, электроника и продукты не кладут в одну зону, иначе учёт превращается в хаос.
Пример:
users(id, email, full_name)courses(id, title, price)enrollments(id, user_id, course_id, status)
🔎 Как это происходит на практике:
  • Контекст: у команды есть список требований и набор экранов продукта.
  • Действия: аналитик выделяет существительные (пользователь, курс, запись, платёж) и проверяет, являются ли они самостоятельными объектами.
  • Результат: формируется список таблиц без смешения разных сущностей в одной структуре.
Характеристики:
  • у каждой таблицы один смысл;
  • поля внутри таблицы описывают только эту сущность;
  • пересечения между таблицами оформляются связями, а не дублированием колонок.
Когда использовать: Всегда, когда проектируете новую схему или рефакторите старую таблицу-«монолит».
Вывод: Правильное выделение сущностей — первый фильтр от будущих дубликатов и противоречивых данных.

🧾 2. Проектирование столбцов: обязательность, тип, смысл

После разделения на таблицы важно не превратить каждую таблицу в «набор колонок без правил».
Именно на уровне столбцов решается, какие данные обязательны, как они проверяются и насколько стабильны запросы.
🟢 Если совсем просто: У каждого поля должен быть понятный тип и причина, зачем оно вообще существует.
🎯 Как понять, что этап прошёл успешно: Для каждого столбца вы можете ответить на три вопроса: что хранит, обязателен ли, как валидируется.
Назначение: Зафиксировать структуру данных внутри сущности так, чтобы она отражала реальные бизнес-правила.
Простыми словами: Мы заранее решаем, какие поля обязательны и какого они формата, чтобы не хранить «мусор».
Для новичка: Если поле всегда нужно для работы сценария, ставьте NOT NULL; если значение может отсутствовать по смыслу, допускайте NULL.
Аналогия: Это как анкета: паспортные данные обязательны, а «любимый цвет» можно не заполнять.
Пример:
CREATE TABLE users (  id BIGSERIAL PRIMARY KEY,  email TEXT NOT NULL UNIQUE,  full_name TEXT NOT NULL,  phone TEXT,  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());
🔎 Как это происходит на практике:
  • Контекст: команда знает, какие поля приходят из UI и API.
  • Действия: для каждого поля выбирают тип, обязательность и ограничения (UNIQUE, CHECK, DEFAULT).
  • Результат: данные приходят в БД в предсказуемом и валидном формате.
Характеристики:
  • обязательные поля защищены NOT NULL;
  • значения с риском дублирования защищены UNIQUE;
  • значения с диапазоном проверяются через CHECK;
  • для событий во времени обычно берут TIMESTAMPTZ, чтобы не терять часовой контекст;
  • для денег используют NUMERIC, а не FLOAT, чтобы избежать ошибок округления;
  • ограничения длины текста задают осознанно: VARCHAR(n), TEXT + CHECK, либо domain-тип.
Когда использовать: На этапе проектирования любой новой таблицы и при аудите текущей схемы.
Вывод: Качество таблицы определяется не количеством колонок, а тем, насколько чётко прописаны правила для каждой.

🔗 3. Проектирование связей: 1-1, 1-N, N-N без «магии»

Связи делают схему реляционной: отдельные таблицы начинают работать как единая модель данных.
Если связи не спроектировать, система быстро получает «осиротевшие» записи и противоречивую аналитику.
🟢 Если совсем просто: Связь показывает, как строки из разных таблиц относятся друг к другу.
🎯 Как понять, что этап прошёл успешно: Каждая ссылка между таблицами оформлена внешним ключом, а не «договорённостью в коде».
Назначение: Обеспечить корректные отношения между сущностями и контроль целостности ссылок.
Простыми словами: Мы явно говорим БД: «эта запись обязана ссылаться на существующую запись в другой таблице».
Для новичка: Если у заказа есть пользователь, то orders.user_id должен всегда указывать на существующего пользователя в users.
Аналогия: Это как билет на поезд: номер вагона должен существовать в расписании, иначе билет недействителен.
Пример:
CREATE TABLE courses (  id BIGSERIAL PRIMARY KEY,  title TEXT NOT NULL,  price NUMERIC(10,2) NOT NULL CHECK (price >= 0)); CREATE TABLE enrollments (  id BIGSERIAL PRIMARY KEY,  user_id BIGINT NOT NULL REFERENCES users(id),  course_id BIGINT NOT NULL REFERENCES courses(id),  status TEXT NOT NULL);
🔎 Как это происходит на практике:
  • Контекст: известно, кто на кого «ссылается» в бизнес-процессах.
  • Действия: выбирают тип связи (1-1, 1-N, N-N) и закрепляют его через FOREIGN KEY.
  • Результат: БД блокирует запись, если ссылка ведёт на несуществующую строку.
Характеристики:
  • 1-N: одна сущность связана со многими (пользователь -> записи);
  • N-N: связь через таблицу-связку;
  • поведение при удалении задаётся явно (RESTRICT, CASCADE, SET NULL);
  • в большинстве OLTP-систем связи фиксируют FOREIGN KEY;
  • исключения (staging/raw-слой, межсервисные границы, шардинг) должны быть осознанными и документированными.
Когда использовать: Как только в системе появляется более одной таблицы с логической зависимостью.
Вывод: Для большинства OLTP-сценариев FOREIGN KEY — базовая защита от битых ссылок, а исключения требуют явного архитектурного обоснования.

🧩 4. Таблицы-связки и бизнес-правила на уровне схемы

Многие реальные сценарии нельзя корректно описать прямой связью: например, пользователь может купить много курсов, и курс может быть куплен многими пользователями.
В таких случаях нужна отдельная таблица-связка, где можно хранить статус, дату, цену на момент покупки и другие параметры отношения.
🟢 Если совсем просто: Когда объектов «много ко многим», делаем отдельную таблицу для их связи.
🎯 Как понять, что этап прошёл успешно: Связь N-N описана отдельной таблицей, а повторные комбинации защищены уникальностью.
Назначение: Корректно хранить связи N-N и дополнительные бизнес-атрибуты самой связи.
Простыми словами: Таблица-связка — это «журнал отношений»: кто, с чем, когда и в каком статусе связан.
Для новичка: Таблица-связка бывает двух видов: «чистая» (только два ключа) и «с атрибутами» (ключи + статус/дата/метрики).
Аналогия: Это как договор между двумя сторонами: сам договор хранится отдельно и содержит условия.
Пример:
CREATE TABLE user_courses (  user_id BIGINT NOT NULL REFERENCES users(id),  course_id BIGINT NOT NULL REFERENCES courses(id),  PRIMARY KEY (user_id, course_id)); CREATE TABLE enrollment_modules (  enrollment_id BIGINT NOT NULL REFERENCES enrollments(id) ON DELETE CASCADE,  module_id BIGINT NOT NULL REFERENCES course_modules(id),  progress_percent INT NOT NULL DEFAULT 0 CHECK (progress_percent BETWEEN 0 AND 100),  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),  PRIMARY KEY (enrollment_id, module_id));
🔎 Как это происходит на практике:
  • Контекст: продукту нужны как «чистые» связи пользователь-курс, так и связи с прогрессом по модулям.
  • Действия: для чистой N-N создают таблицу только с ключами; для N-N с атрибутами добавляют отдельные поля связи.
  • Результат: схема остаётся понятной, и каждая связь хранит ровно тот набор данных, который ей нужен.
Характеристики:
  • хранит два и более внешних ключа;
  • может иметь собственные атрибуты (статус, дата, метрика);
  • часто использует составной PRIMARY KEY или UNIQUE-комбинацию.
Когда использовать: При любой N-N связи и в ситуациях, где у связи есть собственные бизнес-поля.
Вывод: Таблица-связка превращает «неудобную связь» в управляемую структуру с чёткими правилами.

⚙️ 5. Проектирование эволюции схемы: как менять таблицы без боли

Схема никогда не остаётся неизменной: продукт растёт, меняются требования, появляются новые статусы и отчёты.
Если не думать об эволюции, каждая миграция становится риском для продакшена.
🟢 Если совсем просто: Проектировать нужно не только «как сейчас», но и «как это будет меняться».
🎯 Как понять, что этап прошёл успешно: Изменения в схеме делаются миграциями, обратимы и не ломают существующие данные.
Назначение: Подготовить таблицы к безопасным изменениям без потери данных и простоя сервиса.
Простыми словами: Мы закладываем в схему путь роста: чтобы можно было добавлять новое, не переписывая всё с нуля.
Для новичка: Если новое поле добавляется в таблицу без паники и ручных правок данных ночью — эволюция спроектирована правильно.
Аналогия: Это как проектирование дома с резервом под новый этаж: фундамент готов к расширению заранее.
Пример:
ALTER TABLE enrollments  ADD COLUMN canceled_at TIMESTAMPTZ; ALTER TABLE enrollments  ADD CONSTRAINT enrollments_status_check  CHECK (status IN ('active', 'paused', 'completed', 'canceled'));
Варианты моделирования статусов:
  • TEXT + CHECK — быстрый и понятный старт, удобно для большинства небольших/средних схем.
  • ENUM (PostgreSQL) — строгий тип и чистая модель, но изменения списка статусов требуют аккуратных миграций.
  • справочник statuses + FOREIGN KEY — гибко для сложных доменов и админ-управления статусами, но больше JOIN и сложнее схема.
🔎 Как это происходит на практике:
  • Контекст: бизнес просит добавить новый статус и отчётность по отменам.
  • Действия: команда оформляет миграцию, обновляет ограничения и проверяет обратную совместимость.
  • Результат: новая функциональность добавляется без ручного «лечения» старых строк.
Характеристики:
  • изменения вносятся через версионируемые миграции;
  • новые ограничения добавляются после проверки текущих данных;
  • для критичных изменений готовится план отката.
Когда использовать: Всегда, когда меняется структура таблиц, статусы, связи или обязательность полей.
Вывод: Хорошее проектирование учитывает будущее развитие схемы, а не только текущую задачу.

🚀 6. Индексы: проектирование под реальные запросы

Схема считается удачной не только когда «данные правильно пишутся», но и когда они быстро читаются.
Если не учесть индексы на этапе проектирования, то после роста данных даже корректная модель начинает тормозить.
🟢 Если совсем просто: Индекс — это ускоритель поиска, который планируют вместе со схемой, а не после первых жалоб.
🎯 Как понять, что этап прошёл успешно: Для ключевых WHERE/JOIN/ORDER BY запросов есть осмысленные индексы, и план выполнения не уходит в полный скан таблиц без причины.
Назначение: Сделать чтение данных предсказуемо быстрым под реальные пользовательские сценарии.
Простыми словами: Мы заранее определяем, по каким полям будут искать и связывать данные, и ставим индексы именно туда.
Для новичка: Если поле часто участвует в фильтре, JOIN или сортировке, это кандидат на индекс.
Аналогия: Это как алфавитный каталог в библиотеке: книги те же, но нужная находится намного быстрее.
Пример:
CREATE INDEX idx_enrollments_user_status  ON enrollments(user_id, status); CREATE INDEX idx_payments_status_paid_at  ON payments(payment_status, paid_at); SELECT  u.full_name,  c.title,  e.status,  p.payment_statusFROM enrollments eJOIN users u ON u.id = e.user_idJOIN courses c ON c.id = e.course_idLEFT JOIN payments p ON p.enrollment_id = e.idWHERE e.user_id = 42 AND e.status = 'active'ORDER BY e.enrolled_at DESC;
🔎 Как это происходит на практике:
  • Контекст: есть конкретные экраны и API-запросы, которые читают данные чаще всего.
  • Действия: команда выписывает топ-запросы и проектирует индексы под их фильтры и соединения.
  • Результат: схема сразу рассчитана на рабочую нагрузку, а не только на корректность вставки.
Характеристики:
  • индексируют поля частых фильтров (WHERE);
  • индексируют поля соединений (JOIN) и сортировок (ORDER BY) при повторяемых сценариях;
  • внешние ключи часто нуждаются в индексах, особенно на таблицах с большим объёмом данных;
  • лишние индексы тоже вредны: они замедляют запись и усложняют сопровождение.
Когда использовать: При проектировании любой продакшен-схемы, где ожидается регулярное чтение данных через API и отчёты.
Вывод: Проектирование таблиц без индексов — это половина работы: корректность без производительности долго не живёт.

⚖️ Сравнение подходов к проектированию

Одна и та же задача может быть реализована по-разному: «сразу писать SQL» или «сначала проектировать модель».
Ниже сравнение показывает, почему второй путь почти всегда дешевле на дистанции.
🟢 Если совсем просто: Проектирование до реализации экономит время после релиза.
🎯 Как понять, что этап прошёл успешно: Команда может объяснить, почему выбрала конкретную структуру, а не просто «так получилось».
Пример сравнения:
ПодходПлюсыМинусыКогда уместен
Сразу писать SQLБыстрый стартВысокий риск переделок и дубликатовТолько для временных прототипов
Сначала модель, потом SQLСтабильная схема, меньше баговНужен стартовый анализПродакшен и долгоживущие проекты
Смешанный (черновик + ревью)Баланс скорости и качестваТребует дисциплины командыКоманды с короткими спринтами
Вывод: Чем дольше живёт проект, тем дороже обходится отказ от нормального проектирования таблиц.

📌 Must-know факты

Эти пункты нельзя пропускать: именно на них чаще всего «падает» качество данных и собеседования.
Понимание этих правил отличает рабочую схему от «демо-таблиц».
🟢 Если совсем просто: Это минимальный набор, без которого проектирование таблиц считается незавершённым.
🎯 Как понять, что этап прошёл успешно: Вы можете применить каждый пункт к своей схеме и показать конкретный SQL-пример.
  • Одна таблица должна описывать одну сущность, а не «всё сразу».
  • У каждой таблицы должен быть надёжный первичный ключ.
  • В большинстве OLTP-схем связи фиксируют внешними ключами; исключения должны быть документированы.
  • Связь N-N всегда оформляется отдельной таблицей-связкой.
  • Бизнес-критичные поля должны иметь NOT NULL, UNIQUE или CHECK при необходимости.
  • Нельзя полагаться только на валидацию в приложении: правила должны быть и в БД.
  • Изменения структуры в продакшене нужно делать через миграции, а не ручные правки.
  • Типы данных выбираются по смыслу и будущим запросам, а не «чтобы работало сейчас».
  • Индексы планируют под реальные сценарии чтения (WHERE, JOIN, ORDER BY), а не добавляют случайно.
Вывод: Если хотя бы один must-know пункт пропущен, схема обычно начинает ломаться при первом росте нагрузки.

🧨 Частые мифы

В проектировании таблиц много «удобных» мифов, которые сначала ускоряют, а потом дорого обходятся.
Разберём самые популярные ошибки мышления.
🟢 Если совсем просто: Быстрое решение без структуры почти всегда превращается в технический долг.
🎯 Как понять, что этап прошёл успешно: Вы можете аргументированно объяснить, почему миф неверен на реальном примере.
Миф: Чем меньше таблиц, тем проще поддержка. ✅ Как правильно: Простота достигается не количеством таблиц, а понятными границами сущностей и явными связями. 📎 Почему это важно: Одна «супертаблица» быстро превращается в источник дубликатов и конфликтов данных.
Миф: FOREIGN KEY можно не ставить, если «код и так контролирует связи». ✅ Как правильно: Связи должны контролироваться и кодом, и БД; БД — последняя линия защиты. 📎 Почему это важно: Без FK в базе появляются ссылки на несуществующие записи, которые ломают аналитику.
Миф: NULL можно ставить везде, потом разберёмся. ✅ Как правильно: NULL допустим только там, где отсутствие значения реально допустимо по бизнес-логике. 📎 Почему это важно: Избыточные NULL усложняют запросы, отчёты и проверку корректности данных.
Миф: Сначала сделаем «как получится», а проектирование добавим позже. ✅ Как правильно: Минимальное проектирование нужно до первой миграции в продакшен. 📎 Почему это важно: Переделка живой схемы с данными всегда дороже, чем аккуратный старт.

❓ Часто спрашивают на собеседованиях

Ниже вопросы, которые часто задают на junior/middle интервью по SQL и проектированию данных.
Отвечайте через логику «проблема -> решение -> эффект», а не только определениями.
🟢 Если совсем просто: Работодатель проверяет, умеете ли вы проектировать схему под бизнес, а не только писать SELECT.
🎯 Как понять, что этап прошёл успешно: Вы можете объяснить ответ на каждом вопросе на примере реального проекта.
Вопрос: С чего вы начинаете проектирование таблиц? ✅ Ответ: С выделения бизнес-сущностей и сценариев, затем фиксирую ключи, связи и ограничения, и только после этого пишу DDL.
Вопрос: Почему нельзя хранить пользователей и заказы в одной таблице? ✅ Ответ: Это смешивает разные сущности, приводит к дублированию данных и усложняет обновления, фильтрацию и контроль целостности.
Вопрос: Когда использовать таблицу-связку? ✅ Ответ: Когда связь между сущностями типа N-N и/или когда у самой связи есть собственные атрибуты (статус, дата, цена, прогресс).
Вопрос: Где должна жить валидация: в приложении или в БД? ✅ Ответ: В обоих местах: приложение даёт удобные сообщения, а БД гарантирует, что невалидные данные не будут записаны.
Вопрос: Как выбрать между суррогатным и составным ключом? ✅ Ответ: Суррогатный ключ удобен для большинства таблиц, а составной часто уместен в таблицах-связках, где важна уникальность пары значений.
Вопрос: Что важнее на старте: нормализация или скорость разработки? ✅ Ответ: Нужен баланс, но базовую нормализацию и ключевые ограничения откладывать нельзя, иначе скорость быстро превращается в технический долг.
Вопрос: Какие поля индексировать в первую очередь? ✅ Ответ: В первую очередь поля частых фильтров и JOIN (user_id, status, created_at и т.д.), а также сортировки в ключевых запросах.
Вопрос: Как выбрать модель статусов: TEXT + CHECK, ENUM или справочник? ✅ Ответ: TEXT + CHECK удобен для старта, ENUM даёт строгую типизацию, а справочник с FK подходит для сложных и часто меняющихся доменных статусов.
Вопрос: Бывают ли случаи, когда FK не ставят? ✅ Ответ: Да, но это осознанные исключения (staging/raw-слой, границы сервисов, шардинг), которые должны быть зафиксированы архитектурно и компенсированы проверками.
Вопрос: Как понять, что таблица спроектирована плохо? ✅ Ответ: Признаки: много NULL «без причины», дубли, неоднозначные поля, ручные связи по тексту и частые костыли в запросах.
Вопрос: Как безопасно менять структуру таблицы в продакшене? ✅ Ответ: Через миграции с проверкой текущих данных, планом отката и тестом ключевых сценариев чтения/записи.

🚫 Типичные ошибки

Даже при базовом опыте команды часто повторяют одинаковые ошибки проектирования.
Ниже список типичных промахов и правильный способ их исправлять.
🟢 Если совсем просто: Каждая ошибка ниже увеличивает стоимость поддержки и риск багов.
🎯 Как понять, что этап прошёл успешно: Вы проверили свою схему по каждому пункту и устранили найденные риски.

Ошибка 1: проектировать таблицы только «под текущий экран»

Неправильно: Добавлять поля под один UI-сценарий без учёта остальной бизнес-логики.
Правильно: Проектировать таблицы от сущностей и процессов, а не от конкретного экрана.
Почему: Экран может измениться через неделю, а схема БД останется надолго.

Ошибка 2: хранить связь между сущностями в текстовом поле

Неправильно: Связывать таблицы по email, title или другому нестабильному тексту.
Правильно: Использовать числовые/UUID-ключи и FOREIGN KEY.
Почему: Текстовые значения меняются и не дают надёжной ссылочной целостности.

Ошибка 3: откладывать ограничения «на потом»

Неправильно: Сначала создать таблицы без ограничений, рассчитывая на ручной контроль.
Правильно: Сразу задавать минимум ограничений: PK, FK, NOT NULL, UNIQUE.
Почему: Чем дольше схема живёт без правил, тем больше невалидных данных копится.

Ошибка 4: не учитывать будущие запросы и аналитику

Неправильно: Выбирать поля и структуру без понимания, как данные будут читаться.
Правильно: Учитывать основные сценарии выборки и отчётов уже на этапе проектирования.
Почему: Схема, неудобная для чтения, заставляет писать сложные и медленные запросы.

Ошибка 5: менять структуру таблиц вручную в продакшене

Неправильно: Запускать случайные ALTER прямо на боевой базе без версионирования.
Правильно: Использовать миграции, ревью SQL и проверку на стейдже.
Почему: Ручные изменения сложно воспроизвести и почти невозможно надёжно откатить.

✅ Best Practices

Лучшие практики не про «идеальную теорию», а про решения, которые реально работают в продакшене.
Используйте этот список как чек-лист при ревью схемы.
🟢 Если совсем просто: Best practices уменьшают количество сюрпризов после релиза.
🎯 Как понять, что этап прошёл успешно: Схема проходит ревью без критичных замечаний по целостности и читаемости.
  • Начинайте с бизнес-сценариев и сущностей, а не с колонок.
  • Для каждой таблицы фиксируйте ключ, обязательные поля и ограничения.
  • Держите стиль именования единым по всей схеме (snake_case, понятные названия).
  • Явно документируйте кардинальность связей (1-1, 1-N, N-N).
  • Делайте таблицы-связки для N-N и ограничивайте дубли парой ключей.
  • Проверяйте схему на реальных сценариях вставки/обновления/удаления.
  • Используйте миграции как единственный способ изменения структуры.
  • Согласовывайте схему с командой до первого продакшен-релиза.

🧾 Заключение

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

Ключевые мысли

  • Хорошая схема начинается с сущностей и процессов, а не с случайных полей.
  • Ключи и связи должны быть явными и проверяемыми на уровне БД.
  • Таблицы-связки и ограничения защищают данные от дубликатов и противоречий.
  • Миграции и документирование схемы обязательны для безопасного развития проекта.
Если вы проектируете таблицы как «контракт данных», система растёт предсказуемо и без хаоса.
🎯

Проверьте знания

Закрепите материал — пройдите тест по теме «БЛОК 1. Основы реляционных БД — 2. Проектирование таблиц»

Пройти тест →