← назад к разделу

Когда в команде нет единого соглашения по именованию, база данных постепенно превращается в хаос: половина таблиц с заглавной буквы, половина со строчной, индексы без имён, is_deleted рядом с deletedAt. Разобраться в такой схеме через полгода — отдельная задача.

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

Регистр: snake_case без кавычек

PostgreSQL приводит все имена к нижнему регистру, если только они не взяты в двойные кавычки. Поэтому OrderDoc, orderDoc и orderdoc — это одно и то же в PostgreSQL. А вот "OrderDoc" в кавычках — совсем другое: PG сохраняет регистр и требует кавычек в каждом запросе.

Правило простое: используйте snake_case без кавычек для всех объектов — таблиц, колонок, индексов, функций, constraints.

-- правильно
CREATE TABLE order_doc (
    id           bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    customer_id  bigint NOT NULL,
    created_at   timestamptz NOT NULL DEFAULT now()
);

-- частая ошибка
CREATE TABLE OrderDoc (
    Id          bigint,
    customerId  bigint,
    "CreatedAt" timestamptz
);

"CreatedAt" с кавычками — это ловушка: теперь во всех запросах нужно писать кавычки, иначе PG не найдёт колонку.

Таблицы: единственное число, существительные

Таблица описывает тип объекта, а не коллекцию. Поэтому имя — существительное в единственном числе: order_doc, customer, product, payment.

Множественное число (orders, customers) тоже встречается и логически не ошибочно, но смешивать подходы в одной базе — плохо. Выберите одно соглашение и придерживайтесь его везде.

Таблицы связей (M:N) называют по обеим сущностям: order_item, customer_role, product_tag.

В крупных схемах с несколькими доменами удобно добавлять префикс домена: order_*, catalog_* — или выносить домены в отдельные схемы PostgreSQL.

Колонки: суффиксы говорят о типе

Хорошее имя колонки сразу понятно без DDL. Несколько соглашений, которые в этом помогают:

Первичный ключ — просто id, без имени таблицы. В таблице customer первичный ключ — id, а не customer_id. Имя с таблицей customer_id — это формат для внешних ключей.

Внешние ключи<родительская_таблица>_id: customer_id, order_id.

Boolean — с префиксом is_, has_, can_: is_active, has_avatar, can_publish. Без префикса непонятно: active — это статус или действие?

Временны́е метки — суффикс _at для timestamp: created_at, updated_at, expires_at. Для дат без времени — без суффикса или _on: born_on, holiday_date.

Деньги — суффиксы _amount, _price, _rate: total_amount, discount_rate. Без суффикса price — непонятно, это сумма или процент.

Длительности — явная единица измерения: ttl_seconds, delivery_days, session_timeout_ms. Просто delivery_time integer — сколько это? Секунды? Минуты? Часы?

Счётчики — суффикс _count: view_count, items_count.

Enum-статусы — без суффикса: status, type, currency.

created_at   timestamptz NOT NULL DEFAULT now(),
updated_at   timestamptz NOT NULL DEFAULT now(),
expires_at   timestamptz,
born_on      date,
ttl_seconds  integer,
total_amount numeric(15,2),
is_active    boolean NOT NULL DEFAULT true,
view_count   integer NOT NULL DEFAULT 0

Audit-колонки и soft-delete

Стандартный набор для отслеживания истории:

created_at   timestamptz NOT NULL DEFAULT now(),
updated_at   timestamptz NOT NULL DEFAULT now(),
created_by   bigint REFERENCES customer(id),
updated_by   bigint REFERENCES customer(id),
version      bigint NOT NULL DEFAULT 0

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

Мягкое удаление — частая ошибка — хранить is_deleted boolean. Лучше deleted_at timestamptz:

-- правильно: момент удаления сохранён
deleted_at   timestamptz   -- NULL = запись существует

-- частая ошибка: момент удаления потерян навсегда
is_deleted   boolean

Из deleted_at тривиально получить boolean: WHERE deleted_at IS NULL. А вот из is_deleted = true время удаления уже не восстановить.

Индексы и constraints: префикс по типу

Когда у индекса нет имени, PostgreSQL генерирует его сам: order_doc_pkey, order_doc_customer_id_idx. Это приемлемо для первичных ключей, но для остальных объектов лучше задавать имена явно — тогда сообщения об ошибках будут понятными.

Соглашение по префиксам:

ТипПрефиксПример
Обычный индексix_ix_order_customer_id
Уникальный индексuk_uk_customer_email
Foreign keyfk_fk_order_item_order_id
Check constraintck_ck_order_total_positive
Primary keypk_ (обычно авто)
Триггерtr_tr_order_doc_audit
CREATE INDEX ix_order_customer_id ON order_doc (customer_id);
CREATE INDEX ix_order_status_created_at ON order_doc (status, created_at);
CREATE UNIQUE INDEX uk_customer_email ON customer (email);

-- функциональный индекс
CREATE INDEX ix_account_email_lower ON account ((lower(email)));

-- частичный индекс
CREATE INDEX ix_order_active ON order_doc (customer_id) WHERE status IN ('NEW','PAID');

ALTER TABLE order_item
    ADD CONSTRAINT fk_order_item_order_id
    FOREIGN KEY (order_id) REFERENCES order_doc(id);

CONSTRAINT ck_order_total_positive CHECK (total_amount >= 0)

Явное имя у CHECK constraint сразу объясняет ошибку: CHECK constraint violated: ck_order_total_positive — понятно, какое правило нарушено.

Зарезервированные слова

PostgreSQL держит список слов, которые нельзя использовать как идентификаторы без кавычек: user, order, group, type, position, value, name, default, desc, asc, start, end, class.

Если назвать таблицу user — каждый запрос придётся писать с кавычками: SELECT * FROM "user". Это неудобно и легко сломать.

Хорошие альтернативы:

  • «Пользователь» → customer, account, person
  • «Заказ» → order_doc, purchase, shipment

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

SELECT * FROM pg_get_keywords() WHERE catcode IN ('R', 'T');

Длина имени

PostgreSQL молча обрезает имена длиннее 63 символов (это ограничение NAMEDATALEN - 1). Обрезание не вызывает ошибки — имя просто станет другим, и возможна коллизия с уже существующим объектом.

Практическое правило: держать имена до 30 символов. Если используете сокращения — придерживайтесь одного варианта по всему проекту: если usr — то везде usr, не мешайте с user и account.

Sequences и IDENTITY

Если использовать GENERATED ALWAYS AS IDENTITY, PostgreSQL автоматически создаст sequence с именем <таблица>_<колонка>_seq. Это имя трогать не нужно.

Обращаться к nextval('customer_id_seq') напрямую имеет смысл только при ручном импорте данных — в остальных случаях IDENTITY управляет sequence самостоятельно.

View и materialized view

Представления называют с суффиксом _v, материализованные — с суффиксом _mv:

CREATE VIEW customer_active_v AS
    SELECT * FROM customer WHERE deleted_at IS NULL AND is_active = true;

CREATE MATERIALIZED VIEW order_stats_mv AS
    SELECT date_trunc('day', created_at) as day, count(*) as cnt
    FROM order_doc GROUP BY 1;

Суффикс помогает сразу отличить view от таблицы при чтении запроса.

Частые ошибки

CamelCase или кавычки — вместо "OrderDoc" пишите order_doc. Без кавычек — case-insensitive, с кавычками — придётся везде их писать.

customer_id как первичный ключ в таблице customer — правило: внутри своей таблицы ключ всегда id.

is_deleted boolean для мягкого удаления — используйте deleted_at timestamptz, момент удаления будет сохранён.

Число без единицыdelivery_time integer не говорит ничего. Пишите delivery_days или delivery_seconds.

Зарезервированные слова без кавычекCREATE TABLE user не заработает. Переименуйте в account или customer.

Служебный префикс tbl_tbl_orders — PostgreSQL и так знает, что это таблица. Префикс ничего не добавляет.

Тип данных в имениcreated_timestamp вместо created_at. Тип видно из DDL, имя должно говорить о смысле.

data jsonb — слишком абстрактно. Назовите по смыслу: attributes, payload, metadata, config.

FK и CHECK без явных имён — автоматические имена нечитаемые, явные имена дают понятные сообщения об ошибках.

Коротко

  • Всё в snake_case, никаких кавычек — иначе PG начнёт различать регистр.
  • Таблицы — существительные в единственном числе: order_doc, customer.
  • Первичный ключ — id; внешний ключ — <родительская_таблица>_id.
  • Boolean с is_/has_/can_, временны́е метки с _at, деньги с _amount/_price.
  • Длительности — с единицей: ttl_seconds, delivery_days.
  • Мягкое удаление — deleted_at timestamptz, не is_deleted boolean.
  • Индексы и constraints — с префиксом ix_/uk_/fk_/ck_ и явным именем.
  • Зарезервированные слова (user, order, group) — не использовать как имена.
  • Имена длиннее 63 символов PostgreSQL обрежет молча.
  • View — суффикс _v, materialized view — _mv.

Что почитать дальше

  • Типы индексов в PostgreSQL — какой индекс выбрать для задачи.
  • Composite-индексы и левый префикс — порядок колонок в индексе.
  • Миграции без даунтайма — как безопасно переименовать колонку.
  • Время и таймзоны в PostgreSQL — почему всегда timestamptz.