$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогПроектирование схемы таблиц: 10 ошибок новичков и как их исправить

Проектирование схемы таблиц: 10 ошибок новичков и как их исправить

$ sudo teach IT
·24 июля 2026 г.·11 мин·47
Проектирование схемы таблиц: 10 ошибок новичков и как их исправить

Разберём типовые проблемы моделирования: неверные ключи, денормализация без причины, смешивание сущностей и хранение вычисляемых значений. Дадим чек-лист проектирования.

Содержание
1) Неправильные ключи: отсутствие PK или «случайный» идентификаторКак выглядит ошибкаПочему это проблемаКак исправить2) Денормализация «на всякий случай»Как выглядит ошибкаПочему это проблемаКак исправить3) Смешивание сущностей в одной таблицеКак выглядит ошибкаПочему это проблемаКак исправить4) Хранение вычисляемых значений как «истины»Как выглядит ошибкаПочему это проблемаКак исправить5) Отсутствие ограничений целостности (FK/UNIQUE/CHECK)Как выглядит ошибкаПочему это проблемаКак исправить6) Плохая модель связей: один-ко-многим превращают в много-ко-многим (и наоборот)Как выглядит ошибкаПочему это проблемаКак исправить7) Неаккуратное проектирование NULL: «пусть будет NULL везде»Как выглядит ошибкаПочему это проблемаКак исправить8) Игнорирование нормализации там, где она нужна (но и не понимают, где денормализация допустима)Как выглядит ошибкаПочему это проблемаКак исправить9) Неправильная уникальность: “UNIQUE на что попало” или отсутствие UNIQUE там, где он обязателенКак выглядит ошибкаПочему это проблемаКак исправить10) Пренебрежение временем и историей: «поля даты» без понимания, что именно моделируетсяКак выглядит ошибкаПочему это проблемаКак исправитьЧек-лист проектирования схемы таблиц (перед тем как “создать базу”)На уровне сущностейНа уровне связейНа уровне атрибутовНа уровне целостности и сопровождения

Проектирование схемы таблиц — это не «рисование сущностей ради красивой базы». Это дисциплина: как хранить данные так, чтобы изменения в бизнесе не превращались в вечный ремонт запросов, а целостность данных держалась на ограничениях, а не на дисциплине пользователей.

Новички чаще всего ломают схему не из‑за отсутствия теории, а из‑за типовых привычек: «сделаю так, чтобы работало», «заведу одну таблицу на всё», «вычислю на стороне приложения», «добавлю поле, потому что нужно в отчёте». В итоге схема либо быстро деградирует, либо начинает требовать сложных костылей.

Ниже — 10 самых распространённых ошибок моделирования, что именно в них опасно, и как исправить с примерами (с учётом типичной реляционной базы, где есть PK/FK/UNIQUE/CHECK и нормализация).


1) Неправильные ключи: отсутствие PK или «случайный» идентификатор

Как выглядит ошибка

  • В таблице нет первичного ключа.
  • PK — это логическое поле, которое может измениться (например, email, name).
  • Используется составной ключ без причины, когда есть простой суррогатный.
  • «ID генерируется на приложении» и периодически ломается в гонках.

Почему это проблема

PK — это фундамент ссылочной целостности и оптимизаций. Когда ключ нестабилен, вы получаете:

  • невозможность корректно ссылаться на запись,
  • дубликаты,
  • дорогие операции обновления и перепривязки,
  • сложные индексы и плохие планы запросов.

Как исправить

  • Для ссылок на сущность используйте стабильный идентификатор: BIGINT/UUID (в зависимости от требований).
  • Естественные ключи (например, email) можно делать UNIQUE, но не использовать как единственный PK.
  • Для связей используйте FK на PK родителя.

Пример: исправление схемы пользователей

code
-- Ошибка: email как PK
-- CREATE TABLE users(email TEXT PRIMARY KEY, ...);

-- Исправление:
CREATE TABLE users (
  id BIGSERIAL PRIMARY KEY,
  email TEXT NOT NULL UNIQUE,
  full_name TEXT
);

2) Денормализация «на всякий случай»

Как выглядит ошибка

Новички часто добавляют дублирующиеся поля в дочерние таблицы:

  • в orders хранят customer_name, хотя есть customers,
  • в order_items — product_price, хотя цена берётся из products,
  • в logs — «удобные» поля вместо нормальных ссылок.

Почему это проблема

Денормализация допустима, но должна быть осознанной:

  • Если данные меняются (имя клиента, категория товара), вы получаете несогласованность.
  • Если обновления редки, но чтения часты — можно думать о денормализации, но тогда нужен регламент синхронизации.

Как исправить

  1. Если значение вычисляется из текущего состояния другой сущности — храните ссылку и вычисляйте в запросе.
  2. Если значение должно отражать исторический факт (например, «сколько стоил товар на момент покупки») — тогда храните значение в момент события, но фиксируйте это явно.

Пример: историческая цена в заказе

code
CREATE TABLE products (
  id BIGSERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  current_price NUMERIC(12,2) NOT NULL
);

CREATE TABLE orders (
  id BIGSERIAL PRIMARY KEY,
  customer_id BIGINT NOT NULL REFERENCES users(id),
  created_at TIMESTAMP NOT NULL DEFAULT now()
);

CREATE TABLE order_items (
  id BIGSERIAL PRIMARY KEY,
  order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
  product_id BIGINT NOT NULL REFERENCES products(id),
  quantity INT NOT NULL CHECK (quantity > 0),
  price_at_purchase NUMERIC(12,2) NOT NULL
);

Здесь price_at_purchase — действительно факт события, а не «текущее состояние».


3) Смешивание сущностей в одной таблице

Как выглядит ошибка

Одна таблица содержит поля, принадлежащие разным типам сущностей:

  • transactions содержит и чеки, и возвраты, и списания, и штрафы, причём набор полей сильно разный;
  • user_data хранит и профиль, и настройки, и история — со множеством NULL.

Иногда это выглядит «удобно», но быстро становится невозможным:

  • валидация превращается в набор сложных правил,
  • запросы начинают содержать CASE WHEN,
  • индексы становятся непредсказуемыми.

Почему это проблема

Разные сущности имеют разные инварианты:

  • разные обязательные поля,
  • разные правила целостности,
  • разные связи.

Смешивание ломает эти инварианты — вы вынуждены выражать логику на уровне приложения.

Как исправить

Выносите сущности в отдельные таблицы, а если нужна полиморфность — используйте отдельные таблицы или модель «тип + подтаблица».

Пример: заказы и возвраты Вместо одной «transactions» с множеством необязательных полей:

  • orders — заказ,
  • refunds — возврат, с FK на orders.
code
CREATE TABLE orders (
  id BIGSERIAL PRIMARY KEY,
  customer_id BIGINT NOT NULL REFERENCES users(id),
  created_at TIMESTAMP NOT NULL DEFAULT now()
);

CREATE TABLE refunds (
  id BIGSERIAL PRIMARY KEY,
  order_id BIGINT NOT NULL REFERENCES orders(id),
  refund_amount NUMERIC(12,2) NOT NULL CHECK (refund_amount > 0),
  created_at TIMESTAMP NOT NULL DEFAULT now()
);

Если требуется «привязка к нескольким типам событий», можно думать о таблице событий events, но это отдельная тема.


4) Хранение вычисляемых значений как «истины»

Как выглядит ошибка

В БД хранят:

  • итоги total_amount,
  • age (который вычисляется из даты рождения),
  • status (который можно вывести из дат и флагов),
  • full_text_search_vector без обновления.

Иногда это делается для скорости, но часто сопровождается тем, что значения забывают пересчитывать.

Почему это проблема

Вычисляемые значения должны быть либо:

  • выводимыми (вычисление в запросе),
  • либо материализованными с понятным механизмом обновления (триггеры, задания, materialized views).

Если хранить вычисляемое без механизма обновления — появляется рассинхрон, который трудно диагностировать: «вроде всё обновляется, но отчёт показывает другое».

Как исправить

  • Если это не «исторический факт» — не храните, вычисляйте.
  • Если нужно хранить для производительности — делайте материализацию осознанно и создайте правила обновления.

Пример: не хранить age

code
CREATE TABLE people (
  id BIGSERIAL PRIMARY KEY,
  birth_date DATE NOT NULL
);

-- В запросе:
SELECT id,
       EXTRACT(YEAR FROM AGE(birth_date)) AS age_years
FROM people;

Пример: материальный итог по событию Если total_amount — итог конкретного заказа и должен фиксироваться, можно хранить total_amount, но тогда фиксируйте его как результат на момент закрытия заказа, и не используйте его как «текущий» пересчитываемый показатель.


5) Отсутствие ограничений целостности (FK/UNIQUE/CHECK)

Как выглядит ошибка

  • Нет внешних ключей (FOREIGN KEY) или отключена проверка.
  • Нет UNIQUE на естественных идентификаторах.
  • Нет CHECK для доменных инвариантов: количество не может быть отрицательным, цена не может быть нулевой и т. п.

Почему это проблема

Без ограничений БД превращается в «хранилище строк». Тогда:

  • дубликаты и «битые ссылки» создаются случайно,
  • ошибки проявляются в приложении поздно,
  • миграции становятся сложнее, потому что данные уже загрязнены.

Как исправить

Добавляйте ограничения:

  • PK: определяет запись.
  • FK: обеспечивает связность.
  • UNIQUE: предотвращает дубликаты.
  • CHECK: защищает инварианты.

Пример: ограничения в справочнике статусов и заказах

code
CREATE TABLE order_statuses (
  id SMALLINT PRIMARY KEY,
  code TEXT NOT NULL UNIQUE
);

CREATE TABLE orders (
  id BIGSERIAL PRIMARY KEY,
  customer_id BIGINT NOT NULL REFERENCES users(id),
  status_id SMALLINT NOT NULL REFERENCES order_statuses(id),
  created_at TIMESTAMP NOT NULL DEFAULT now()
);

ALTER TABLE orders
  ADD CONSTRAINT orders_status_created_not_null
  CHECK (created_at IS NOT NULL);

Хотя created_at и так NOT NULL — пример показывает подход: ограничения должны быть осмысленными и проверяемыми.


6) Плохая модель связей: один-ко-многим превращают в много-ко-многим (и наоборот)

Как выглядит ошибка

  • Встречаются схемы, где user и role реализованы как поле role_id в users, хотя у пользователя может быть несколько ролей.
  • Или наоборот: сделали таблицу user_roles, хотя пользователь имеет ровно одну роль по доменной логике.

Почему это проблема

Ошибка приводит к:

  • невозможности выразить бизнес-правила,
  • неверным запросам и сложным условиям,
  • ошибкам в миграциях (а миграции — самые дорогие изменения).

Как исправить

Определите кардинальности:

  • один к одному (1:1),
  • один ко многим (1:N),
  • многие ко многим (M:N).

Для M:N используйте связующую таблицу с составным PK или UNIQUE на паре FK.

Пример: M:N “пользователи — роли”

code
CREATE TABLE roles (
  id BIGSERIAL PRIMARY KEY,
  code TEXT NOT NULL UNIQUE
);

CREATE TABLE user_roles (
  user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  role_id BIGINT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
  PRIMARY KEY (user_id, role_id)
);

7) Неаккуратное проектирование NULL: «пусть будет NULL везде»

Как выглядит ошибка

  • Поля обязательны по смыслу, но сделаны nullable.
  • Поля nullable используются как “флаги” состояния (например, closed_at = NULL означает открыто, а непустое — закрыто).
  • Иногда вместо справочника статусов делают кучу nullable полей.

Почему это проблема

NULL в SQL — особое значение: оно означает «неизвестно/отсутствует», а не «логический false». Это ломает:

  • простые фильтры,
  • уникальные ограничения,
  • агрегации,
  • простоту чтения кода.

Как исправить

  • Если поле действительно обязательно — делайте NOT NULL.
  • Если это состояние — лучше хранить явный status_id или state (enum/справочник), либо отдельные поля, но с явными ограничениями.

Пример: статус заказа вместо разрозненных NULL

code
CREATE TABLE orders (
  id BIGSERIAL PRIMARY KEY,
  customer_id BIGINT NOT NULL REFERENCES users(id),
  status TEXT NOT NULL CHECK (status IN ('new', 'paid', 'shipped', 'closed'))
);

Если модель требует временных маркеров, можно хранить closed_at как nullable, но тогда status должен быть явным и согласованным с closed_at через CHECK/триггеры (в зависимости от возможностей СУБД).


8) Игнорирование нормализации там, где она нужна (но и не понимают, где денормализация допустима)

Как выглядит ошибка

Обычно это проявляется в двух крайностях:

  • Либо «всё в одну таблицу», где повторяются группы полей (address_line1, address_line2, address_line3 в нескольких местах, если пользователь может иметь несколько адресов).
  • Либо «справочники развели до абсурда», не понимая стоимость JOIN и сложности миграций.

Почему это проблема

Нормализация помогает избежать:

  • аномалий обновления (меняется один факт — нужно менять в десятках строк),
  • аномалий вставки/удаления,
  • дубликатов по смыслу.

Но чрезмерная нормализация может быть неоправданной, особенно если данные действительно принадлежат одной сущности и обновляются вместе.

Как исправить

Ориентируйтесь на инварианты:

  • Если атрибут зависит только от части ключа — выносите.
  • Если атрибуты группы обновляются и живут вместе — они могут оставаться в одной сущности.
  • Денормализацию делайте, когда понимаете, что это оптимизация под конкретные запросы, а не «на будущее».

9) Неправильная уникальность: “UNIQUE на что попало” или отсутствие UNIQUE там, где он обязателен

Как выглядит ошибка

  • UNIQUE установлен на поле, которое может повторяться (например, orders.created_at).
  • Нет UNIQUE там, где требуется доменная уникальность (например, products.sku).
  • Уникальность нужна в контексте (например, email уникален внутри одного тенанта/организации), но модель игнорирует контекст.

Почему это проблема

Неправильные UNIQUE приводят либо к блокировкам и ошибкам вставки, либо к появлению дублей. И то и другое быстро превращается в инциденты.

Как исправить

Определяйте уникальность по доменной логике:

  • глобальная уникальность: UNIQUE(email)
  • составная уникальность: UNIQUE(tenant_id, email)
  • частичная уникальность: уникальность только при определённом статусе — тогда нужны частичные индексы (в PostgreSQL) или дополнительные ограничения/обновление логики.

Пример: email уникален внутри компании

code
CREATE TABLE tenants (
  id BIGSERIAL PRIMARY KEY,
  name TEXT NOT NULL
);

CREATE TABLE users (
  id BIGSERIAL PRIMARY KEY,
  tenant_id BIGINT NOT NULL REFERENCES tenants(id),
  email TEXT NOT NULL,
  UNIQUE (tenant_id, email)
);

10) Пренебрежение временем и историей: «поля даты» без понимания, что именно моделируется

Как выглядит ошибка

  • Нет created_at, updated_at, а аудит нужен.
  • Даты хранятся, но не ясно: это “момент события” или “период действия”.
  • Нет версионности: когда меняется договор/цена, вы затираете прошлое.

Почему это проблема

В реальных системах время — это не просто атрибут, а часть бизнес-семантики. Особенно в продажах, подписках, тарифах, прайс-листах, кадровых изменениях.

Стирание истории делает невозможным расследование:

  • почему отчёт за прошлый месяц другой,
  • какая ставка была действительна тогда,
  • кто и когда изменил данные.

Как исправить

Разделяйте:

  • факт события (paid_at, refunded_at),
  • период действия (valid_from, valid_to),
  • последнюю версию (таблицы версионирования или таблицы “срезов”).

Пример: версионирование тарифов

code
CREATE TABLE plans (
  id BIGSERIAL PRIMARY KEY,
  code TEXT NOT NULL UNIQUE
);

CREATE TABLE plan_prices (
  id BIGSERIAL PRIMARY KEY,
  plan_id BIGINT NOT NULL REFERENCES plans(id),
  valid_from DATE NOT NULL,
  valid_to DATE NOT NULL,
  price NUMERIC(12,2) NOT NULL,
  CHECK (valid_to > valid_from),
  UNIQUE (plan_id, valid_from)
);

Если нужна бизнес-уникальность на интервалах (например, не пересекать периоды) — это уже отдельные ограничения и, возможно, триггеры/исключающие индексы (зависит от СУБД).


Чек-лист проектирования схемы таблиц (перед тем как “создать базу”)

Ниже — практический список вопросов, который помогает быстро поймать 70% ошибок новичков.

На уровне сущностей

  1. Какая сущность? Опишите её инварианты: что всегда верно для записи.
  2. Что является идентификатором? Есть ли стабильный PK? Естественные ключи — как UNIQUE?
  3. Где границы сущностей? Не смешаны ли атрибуты разных концепций в одной таблице?

На уровне связей

  1. Какая кардинальность? 1:1, 1:N или M:N? Если M:N — есть ли таблица-связка?
  2. Есть ли FK и правила удаления? ON DELETE CASCADE/RESTRICT/SET NULL выбран осознанно.
  3. Есть ли индексы под FK? И под поля, по которым чаще всего джоится таблица.

На уровне атрибутов

  1. Это факт или вычисление?
    • Факт события — храните (и фиксируйте момент).
    • Текущее значение — чаще вычисляйте в запросах.
  2. NULL отражает отсутствие/неизвестность или просто “флаг”? Если поле по смыслу обязательно — делайте NOT NULL.
  3. Уникальность — доменная? Где нужны UNIQUE, а где — индексы без уникальности.
  4. Есть ли ограничения CHECK? Количество/цена/даты должны иметь доменные границы.

На уровне целостности и сопровождения

  1. Можно ли нарушить целостность без правок приложения? Если да — значит, нет нужных ограничений.
  2. Что произойдёт при изменениях? Подумайте, как будет выглядеть миграция, если поменяется бизнес-правило.
  3. Как будет выглядеть типичный запрос? Если схема заставляет писать

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

SQL – для начинающих!

Курс по теме

SQL – для начинающих!

С нуля до уверенных запросов на SQL. Никакой теории без дела — только живые запросы, настоящие данные и готовность к работе с любой базой. Хватит гуглить «как написать JOIN» — разберись один раз и забудь.

Бесплатно
Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

SQL: диаграмма жизненного цикла индекса — от EXPLAIN до теста на регрессию производительности
диаграмма

SQL: диаграмма жизненного цикла индекса — от EXPLAIN до теста на регрессию производительности

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

16 сентября 2026 г.
1050
SQL JOIN за 10 минут: наглядный гид с реальными примерами
join

SQL JOIN за 10 минут: наглядный гид с реальными примерами

Объясняем INNER, LEFT, RIGHT и FULL JOIN на живых таблицах — без воды и абстрактных схем, только запросы и результаты.

16 июля 2026 г.
690
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

1) Неправильные ключи: отсутствие PK или «случайный» идентификаторКак выглядит ошибкаПочему это проблемаКак исправить2) Денормализация «на всякий случай»Как выглядит ошибкаПочему это проблемаКак исправить3) Смешивание сущностей в одной таблицеКак выглядит ошибкаПочему это проблемаКак исправить4) Хранение вычисляемых значений как «истины»Как выглядит ошибкаПочему это проблемаКак исправить5) Отсутствие ограничений целостности (FK/UNIQUE/CHECK)Как выглядит ошибкаПочему это проблемаКак исправить6) Плохая модель связей: один-ко-многим превращают в много-ко-многим (и наоборот)Как выглядит ошибкаПочему это проблемаКак исправить7) Неаккуратное проектирование NULL: «пусть будет NULL везде»Как выглядит ошибкаПочему это проблемаКак исправить8) Игнорирование нормализации там, где она нужна (но и не понимают, где денормализация допустима)Как выглядит ошибкаПочему это проблемаКак исправить9) Неправильная уникальность: “UNIQUE на что попало” или отсутствие UNIQUE там, где он обязателенКак выглядит ошибкаПочему это проблемаКак исправить10) Пренебрежение временем и историей: «поля даты» без понимания, что именно моделируетсяКак выглядит ошибкаПочему это проблемаКак исправитьЧек-лист проектирования схемы таблиц (перед тем как “создать базу”)На уровне сущностейНа уровне связейНа уровне атрибутовНа уровне целостности и сопровождения