Конвенции именования — самая дешёвая и недооценённая дисциплина. Они не дают ускорения, но через два года команда легко читает чужой код, а DBA не путается в EXPLAIN. Эта статья — что назвать как и почему.

Правила пронумерованы кодами PG-N-NNN — на них ссылается скилл ucp-pg-schema-review.

1. Регистр и стиль

PG-N-001 snake_case для всего: таблиц, колонок, индексов, constraint'ов, sequence'ов, view'ов.

-- правильно
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
);

PG-N-002 Никаких кавычек в идентификаторах. "OrderDoc" (с двойными кавычками) принуждает PG сохранить регистр и требует кавычек в каждом запросе. order_doc без кавычек — case-insensitive, удобно. Кавычки нужны только для конфликтных слов (см. §6).

2. Таблицы

PG-N-010 Существительные в единственном числе: order_doc, customer, product, payment, не orders/customers/products.

Аргумент: ОДНА строка — это ОДНА сущность (SELECT * FROM customer WHERE id = 1 возвращает одного клиента). jOOQ-генерация даёт Customer класс из customer таблицы — натуральнее Orders из orders.

Это спорная конвенция (Hibernate/JPA-сообщество чаще предпочитает plural). Главное правило — выбрать одно и не смешивать. Если 80% таблиц singular, новые тоже singular.

PG-N-011 Junction-таблицы (many-to-many) — обе сущности в порядке: order_item, customer_role, product_tag.

PG-N-012 Префикс домена для крупных схем. Если в одной БД живут несколько Bounded Context'ов — префикс схемы (order_*, catalog_*) или отдельный schema (order.doc, catalog.product). Без префиксов 200 таблиц одного public нечитаемо.

3. Колонки

PG-N-020 id-колонка таблицы — id. Не customer_id в таблице customer.

CREATE TABLE customer (
    id  bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY  -- не customer_id
);

В FK уже видно из имени родителя: order.customer_id REFERENCES customer(id). Удвоение префикса избыточно.

PG-N-021 Foreign key column — <parent>_id. customer_id, order_id, product_id. Это автоматически документирует таблицу-родителя.

PG-N-022 Boolean-колонки — с префиксом is_/has_/can_.

is_active   boolean NOT NULL DEFAULT true,
is_deleted  boolean NOT NULL DEFAULT false,
has_avatar  boolean NOT NULL DEFAULT false,
can_login   boolean NOT NULL DEFAULT true

Без префикса active boolean ↔ active enum трудно отличить в коде. is_active всегда читается как «да/нет».

PG-N-023 Время — глагол прошедшего времени + _at для timestamp, без — для дат:

created_at   timestamptz   -- момент создания
updated_at   timestamptz   -- последнее обновление
deleted_at   timestamptz   -- soft-delete
expires_at   timestamptz   -- момент истечения
published_at timestamptz   -- момент публикации (NULL = не опубликовано)
born_on      date          -- дата рождения (без времени)
hired_on     date

PG-N-024 Денежные колонки — _amount суффикс для сумм, _price для цен, _rate для курсов/процентов.

total_amount      numeric(15, 2)  -- общая сумма
shipping_price    numeric(15, 2)  -- цена доставки
discount_rate     numeric(5, 4)   -- 0.1500 = 15%
exchange_rate     numeric(20, 8)  -- курс валют

PG-N-025 Длительность — _seconds/_ms/_days/_hours суффикс с явной единицей.

ttl_seconds       integer
delivery_days     integer
session_timeout_ms bigint

Без явной единицы измерения через год никто не помнит, в чём delivery_time — секунды, минуты или дни.

PG-N-026 Перечисления — без префикса/суффикса: status, type, kind, role, currency. В Java мапится на одноимённый enum.

PG-N-027 Опционально размер коллекции — _count суффикс:

view_count    integer NOT NULL DEFAULT 0
likes_count   integer NOT NULL DEFAULT 0
items_count   integer NOT NULL DEFAULT 0

4. Audit-колонки

PG-N-030 Стандартный набор для каждой бизнес-таблицы:

created_at   timestamptz NOT NULL DEFAULT now(),
updated_at   timestamptz NOT NULL DEFAULT now(),
created_by   bigint REFERENCES customer(id),    -- или uuid, если auth-user — UUID
updated_by   bigint REFERENCES customer(id),
version      bigint NOT NULL DEFAULT 0          -- для optimistic locking

Не обязательно все четыре сразу, но имена должны быть стандартными. version — для optimistic locking, created_by/updated_by — для аудита.

PG-N-031 Soft-delete — deleted_at timestamptz (NULL = не удалено), не is_deleted boolean. Время удаления — отдельная информация, которая часто нужна. is_deleted означает «когда-то удалили, момент потеряли».

-- хорошо
deleted_at  timestamptz  -- NULL = существует, иначе момент удаления

-- плохо
is_deleted  boolean      -- момент удаления потерян

Из deleted_at тривиально получить boolean: WHERE deleted_at IS NULL.

5. Индексы и constraints

PG-N-040 Префикс по типу — ix_ / uk_ / ck_ / fk_ + таблица + колонки:

ТипПрефиксПример
Обычный индексix_ix_orders_customer_id, ix_orders_status_created_at
Уникальныйuk_uk_customer_email, uk_product_sku
Foreign keyfk_fk_order_item_order_id, fk_order_customer_id
Checkck_ck_order_total_positive, ck_age_range
Primary keypk_pk_customer (обычно автоматически <table>_pkey)
Triggertr_tr_order_doc_audit

PG-N-041 Индекс на одну колонку: ix_<table>_<column>.

CREATE INDEX ix_order_customer_id ON order_doc (customer_id);

PG-N-042 Composite-индекс на несколько: ix_<table>_<col1>_<col2> в порядке полей в индексе.

CREATE INDEX ix_order_status_created_at ON order_doc (status, created_at);

PG-N-043 Functional index — суффикс с операцией:

CREATE INDEX ix_account_email_lower ON account ((lower(email)));
CREATE INDEX ix_event_log_payload_gin ON event_log USING gin (payload jsonb_path_ops);

PG-N-044 Partial index — суффикс c фильтром:

CREATE INDEX ix_order_active_customer ON order_doc (customer_id) WHERE status IN ('NEW','PAID','SHIPPED');
-- альтернатива: ix_order_customer_active

PG-N-045 Constraint name на foreign key — fk_<child>_<column>.

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

Без явного имени PG генерит order_item_order_id_fkey — длиннее и менее читаемо в логах ошибок.

PG-N-046 Check constraint — ck_<table>_<rule>:

CHECK (total_amount >= 0)                     -- автоген имени, плохо
CONSTRAINT ck_order_total_positive CHECK (total_amount >= 0)  -- хорошо
CONSTRAINT ck_product_stock_non_negative CHECK (stock >= 0)

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

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

PG-N-050 Не используй имена-зарезервированные слова PG: user, order, group, type, position, value, name, default, desc, asc, start, end, class.

Если бизнес-домен — «пользователь», бери customer/account/person. Если «заказ» — order_doc/purchase/shipment. Иначе каждый запрос — SELECT * FROM "user" с кавычками или магической ошибкой синтаксиса.

PG-N-051 type/name/value как имена колонок допустимы, но используй с осторожностью. PG их не считает зарезервированными в контексте колонки, но в Java type/name могут конфликтовать с reflection / Jackson.

Полный список ключевых слов: SELECT * FROM pg_get_keywords() WHERE catcode IN ('R', 'T').

7. Длина имени и читаемость

PG-N-060 Лимит PostgreSQL — 63 символа (NAMEDATALEN - 1). PG молча обрежет более длинное имя, что приводит к дубликатам (fk_very_long_table_referencing_another_long_table_some_column_id → коллизия).

PG-N-061 Целевая длина — до 30 символов. Имена в EXPLAIN, в логах ошибок, в Java-генерации (jOOQ создаст IxOrdersStatusCreatedAt класс — короткое имя проще читать).

PG-N-062 Сокращения — единые по проекту. Если решили usr вместо user — везде usr. Mix user/usr/account за год превращается в кашу.

8. Sequences и serial

PG-N-070 GENERATED ALWAYS AS IDENTITY сам генерирует sequence с именем <table>_<column>_seq. Не вмешивайся в это.

CREATE TABLE customer (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY);
-- автоматически: customer_id_seq

Использовать sequence напрямую — антипаттерн. nextval('customer_id_seq') пишут только при ручном импорте данных. В обычных вставках — INSERT INTO customer (name) VALUES (?) без id.

9. View и materialized view

PG-N-080 View — суффикс _v или префикс v_. Materialized view — _mv.

CREATE VIEW customer_active_v AS SELECT * FROM customer WHERE deleted_at IS NULL;
CREATE MATERIALIZED VIEW order_stats_mv AS SELECT customer_id, count(*), sum(total_amount) FROM order_doc GROUP BY customer_id;

Не обязательно, но помогает в EXPLAIN сразу понять, что это не базовая таблица.

10. Анти-паттерны

PG-N-090 tbl_/t_ префикс на таблицах — избыточно (PG и так знает, что это таблица).

PG-N-091 <column>_<datatype> суффикс — name_varchar, created_timestamp — мусор. Тип в DDL, имя — про смысл.

PG-N-092 Зарезервированные имена в кавычках — "user", "order". Лечение — переименовать.

PG-N-093 deleted boolean без _at — теряется момент.

PG-N-094 data / info / details jsonb для основной модели — что в нём, никто не помнит. Имя должно говорить о содержимом: attributes, payload, metadata, config, extra.

PG-N-095 Сокращения по вкусу разработчика — cust_id, usr_nm, cnt. Полные имена короче не делают, читаются хуже.

PG-N-096 Регистр в идентификаторах в кавычках — "OrderDoc". Каждый запрос требует кавычек, мix с lowercase создаёт два разных объекта в одном пространстве.


Чек-лист на ревью схемы

  • [ ] Все идентификаторы в snake_case, без двойных кавычек.
  • [ ] Таблицы — существительные в едином числе (если выбрана эта конвенция; альтернатива — plural везде).
  • [ ] PK-колонка таблицы — id, не <table>_id.
  • [ ] FK-колонка — <parent>_id.
  • [ ] Boolean — с префиксом is_/has_/can_.
  • [ ] Времена — _at для timestamp, _on для date; глагол в прошедшем (created_at, не creation_at).
  • [ ] Деньги — суффикс _amount/_price/_rate.
  • [ ] Длительности — суффикс с единицей (_seconds/_days/_ms).
  • [ ] Audit: created_at, updated_at, опционально created_by, updated_by, version.
  • [ ] Soft-delete через deleted_at timestamptz, не is_deleted boolean.
  • [ ] Индексы: ix_<table>_<cols>; уникальные: uk_; FK: fk_; CHECK: ck_.
  • [ ] Все CHECK constraints имеют явное имя (CONSTRAINT ck_...).
  • [ ] Имена ≤ 30 символов, без зарезервированных слов (user, order, group).
  • [ ] Нет data/info jsonb для основной модели — есть осмысленное имя.

Связанные

  • Числа и точность — типы для _amount/_price.
  • Время и таймзоны — _at колонки в timestamptz.
  • Антипаттерны типов.