Перейти к содержанию
Innovialabs

Типичные ошибки проектирования баз данных и как их избежать

Практика4 мин

Коротко

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

Коротко: какие ошибки встречаются чаще всего

Схема базы данных живёт дольше любого кода вокруг неё. Ошибку в функции можно исправить за час, ошибку в схеме — только миграцией, часто с чисткой уже накопленных данных. Самые частые проблемы:

  • списки в одной колонке вместо отдельной таблицы;
  • нет ограничений — проверки живут только в коде приложения;
  • float для денег;
  • непоследовательные имена таблиц и колонок;
  • нет служебных дат created_at и updated_at;
  • время без часового пояса.

Разберём каждую и покажем исправление на примере PostgreSQL. В MySQL и других СУБД принципы те же, отличается синтаксис.

Ошибка 1: список значений в одной колонке

Колонка tags = 'vip,опт,ташкент' выглядит удобно, пока не понадобится найти всех клиентов с тегом «опт», посчитать их или переименовать тег. Поиск по подстроке медленный и ошибочный: «опт» найдётся и внутри «оптика».

Как исправить: вынесите значения в связующую таблицу (связь «многие ко многим»).

CREATE TABLE tags (
  id   bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name text NOT NULL UNIQUE
);

CREATE TABLE customer_tags (
  customer_id bigint NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
  tag_id      bigint NOT NULL REFERENCES tags(id),
  PRIMARY KEY (customer_id, tag_id)
);

Массивы и JSONB в PostgreSQL допустимы для данных, которые читаются целиком и редко фильтруются. Если по значению нужно искать, связывать или считать — нужна таблица.

Ошибка 2: нет ограничений на уровне базы

«Валидация есть в бэкенде» — до первого скрипта импорта, второго сервиса или ручного UPDATE. Тогда появляются заказы без клиента, отрицательные количества и дубли email.

Что задать в схеме:

  • NOT NULL для обязательных полей;
  • FOREIGN KEY для связей между таблицами;
  • UNIQUE для естественных ключей (email, номер договора, ИНН);
  • CHECK для простых правил: CHECK (quantity > 0).

Ограничения — последняя линия защиты, которая работает для всех клиентов базы одинаково.

Ошибка 3: float для денег

Типы float и double хранят числа в двоичном виде приближённо. Сумма 0.1 + 0.2 в них не равна точно 0.3, и на тысячах операций копейки расходятся с бухгалтерией.

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

  • используйте numeric(14, 2) (или decimal) с явной точностью;
  • либо храните сумму в минимальных единицах (тийины, центы) в bigint;
  • храните валюту рядом с суммой, если валют больше одной: amount numeric(14,2), currency char(3).

Ошибка 4: хаос в именах

Users, order_item, tblProducts, колонка dt и date2 — через год никто не помнит, что где. Плохие имена замедляют каждый запрос и каждого нового разработчика.

Простые правила:

ПравилоПример
Один стиль, обычно snake_caseorder_items, created_at
Таблицы во множественном числе (или везде в единственном)customers, orders
Внешний ключ = таблица + _idcustomer_id
Понятные булевы поляis_active, has_discount
Без сокращений и служебных префиксовне tbl_, не cst_nm

Главное — не конкретный стиль, а одинаковость во всей базе.

Ошибка 5: нет created_at и updated_at

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

created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()

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

Ошибка 6: время без часового пояса

Сервер в одном поясе, пользователи — в Ташкенте и Москве, интеграция присылает время в UTC. Тип timestamp without time zone не знает, к какому поясу относится значение, и отчёты «за день» начинают съезжать на несколько часов.

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

  • храните моменты времени в timestamptz — PostgreSQL сохраняет их в UTC;
  • переводите в локальное время при выводе: created_at AT TIME ZONE 'Asia/Tashkent';
  • для «дат без времени» (день рождения, дата документа) используйте тип date.

Как не допускать ошибок в новых проектах

  1. Нарисуйте ER-диаграмму до написания кода и обсудите её с тем, кто знает бизнес-процесс.
  2. Приведите схему хотя бы к третьей нормальной форме, денормализуйте осознанно.
  3. Меняйте схему только миграциями в репозитории, а не вручную на сервере.
  4. Добавьте в ревью кода чек-лист: ограничения, типы, имена, даты.

Частые вопросы

Да, но поэтапно: добавьте новую структуру, перенесите и проверьте данные, переключите код, затем удалите старое. Ограничения на большие таблицы в PostgreSQL можно добавлять с NOT VALID, а затем проверять отдельно через VALIDATE CONSTRAINT.

Поможем с этим

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

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.