Конвенции именования — самая дешёвая и недооценённая дисциплина. Они не дают ускорения, но через два года команда легко читает чужой код, а 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 key | fk_ | fk_order_item_order_id, fk_order_customer_id |
| Check | ck_ | ck_order_total_positive, ck_age_range |
| Primary key | pk_ | pk_customer (обычно автоматически <table>_pkey) |
| Trigger | tr_ | 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/infojsonb для основной модели — есть осмысленное имя.
Связанные
- Числа и точность — типы для
_amount/_price. - Время и таймзоны —
_atколонки вtimestamptz. - Антипаттерны типов.