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 и ограничивайте дубли парой ключей.
- Проверяйте схему на реальных сценариях вставки/обновления/удаления.
- Используйте миграции как единственный способ изменения структуры.
- Согласовывайте схему с командой до первого продакшен-релиза.
🧾 Заключение
Проектирование таблиц — это точка, где бизнес-логика превращается в надёжные правила хранения данных.
Чем внимательнее вы проектируете сущности, связи и ограничения, тем меньше аварийных фиксов будет в дальнейшем.
Чем внимательнее вы проектируете сущности, связи и ограничения, тем меньше аварийных фиксов будет в дальнейшем.
Ключевые мысли
- Хорошая схема начинается с сущностей и процессов, а не с случайных полей.
- Ключи и связи должны быть явными и проверяемыми на уровне БД.
- Таблицы-связки и ограничения защищают данные от дубликатов и противоречий.
- Миграции и документирование схемы обязательны для безопасного развития проекта.
Если вы проектируете таблицы как «контракт данных», система растёт предсказуемо и без хаоса.