Типичные ошибки проектирования баз данных и как их избежать
Практика4 минРедакция Innovialabs
Коротко
Большинство проблем с данными закладываются в схеме: храните одно значение в одной ячейке, задавайте ограничения на уровне БД, используйте 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_case | order_items, created_at |
| Таблицы во множественном числе (или везде в единственном) | customers, orders |
Внешний ключ = таблица + _id | customer_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.
Как не допускать ошибок в новых проектах
- Нарисуйте ER-диаграмму до написания кода и обсудите её с тем, кто знает бизнес-процесс.
- Приведите схему хотя бы к третьей нормальной форме, денормализуйте осознанно.
- Меняйте схему только миграциями в репозитории, а не вручную на сервере.
- Добавьте в ревью кода чек-лист: ограничения, типы, имена, даты.