Naming & Style Rules
Consistent SQL naming reduces cognitive load across migrations, ORMs, BI tools, and on-call grep sessions. Pick conventions once, document in an ADR, enforce in review.
Search across all documentation pages
Consistent SQL naming reduces cognitive load across migrations, ORMs, BI tools, and on-call grep sessions. Pick conventions once, document in an ADR, enforce in review.
-- Preferred conventions (example team ADR)
CREATE SCHEMA app;
CREATE TABLE app.order ( -- singular table ADR choice
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id bigint NOT NULL REFERENCES app.customer (id),
placed_at timestamptz NOT NULL DEFAULT now(),
status text NOT NULL CHECK (status IN ('open', 'shipped', 'cancelled'))
);
CREATE INDEX idx_order_customer_placed
ON app.order (customer_id, placed_at DESC);When to reach for this: New service bootstrap, linter setup (SQLFluff), or refactoring legacy camelCase schemas.
-- Constraint naming
ALTER TABLE app.order
ADD CONSTRAINT fk_order_customer
FOREIGN KEY (customer_id) REFERENCES app.customer (id);
ALTER TABLE app.order
ADD CONSTRAINT ck_order_status
CHECK (status IN ('open', 'shipped', 'cancelled'));
-- View naming
CREATE VIEW app.v_order_summary AS
SELECT customer_id, count(*) AS order_count
FROM app.order
GROUP BY customer_id;Style guide excerpt:
| Object | Pattern | Example |
|---|---|---|
| Schema | short, lowercase | app, billing |
| Table | singular or plural (pick one) | order vs orders |
| PK column | id | id |
| FK column | {table}_id | customer_id |
| Index | idx_{table}_{cols} | idx_order_customer_placed |
| Unique | uq_{table}_{cols} | uq_customer_email |
| Check | ck_{table}_{rule} | ck_order_status |
| View | v_{name} | v_order_summary |
Singular (order, customer):
Orderorder.customer_idPlural (orders, customers):
Rule: Either is valid - never mix in one database. Existing legacy picks the ADR; greenfield teams choose and document.
-- Avoid
CREATE TABLE app.OrderItems (orderItemId bigint);
-- Prefer
CREATE TABLE app.order_item (order_item_id bigint); -- if singular ADR"camelCase" is a permanent trap.placed_at timestamptz NOT NULL -- instants
amount_cents bigint NOT NULL -- money as integer minor units
currency_code char(3) NOT NULL -- ISO 4217timestamp without time zone for real-world events.amount_cents beats float for money.COMMENT ON TABLE app.order IS 'Customer purchase header; tenant-scoped via RLS';
COMMENT ON COLUMN app.order.status IS 'open|shipped|cancelled; expand via migration';\d+.user, order need quoting or renaming (app_user, sales_order). Fix: Prefix or suffix reserved names.cust_ord_ln saves typing, loses readability. Fix: Prefer full words under 30 chars.orders_customer_id_idx vs idx_order_customer. Fix: One template in SQLFluff config.app with explicit search_path.| Alternative | Use When | Don't Use When |
|---|---|---|
| PostgreSQL ENUM types | Fixed small value set | Values change weekly |
| Domain types | Reuse constraints | Team lacks type discipline |
| Prefix by bounded context | Large monolith DB | Small single-service schema |
Standard is {referenced_table_singular}_id even when table is plural: orders.customer_id if table is orders.
PostgreSQL max identifier 63 bytes - keep names descriptive but under ~40 chars.
Yes - add naming rules to CI for db/migrations/** and rejected ad-hoc SQL in repos.
Stack versions: This page was written for PostgreSQL 18.4 (stable 18, maintenance 17), pgvector 0.8+, PgBouncer 1.x, Patroni 3.x, and PostGIS 3.5+.
Reviewed by Chris St. John·Last updated Jul 18, 2026