Overview
Use PostgreSQL 18 as the default relational store for app data. 18 shipped on 2025-09-25 and 18.6 is the current minor release; 19 is in beta with general availability planned for October 2026; 14 reaches end of life on 2026-11-12. This page holds the cross-cutting rules, and each topic below has its own page. For the head-to-head with MySQL, see postgres-vs-mysql.
Use the Postgres 18 features that change decisions
Check SHOW server_version before relying on any of these.
| Change in 18 | Rule |
|---|---|
uuidv7() | Native time-ordered UUID. Use it for UUID primary keys. |
| B-tree skip scan | A multicolumn index can serve a query with no equality on its leading column. See postgres-indexes. |
| Async I/O | io_method defaults to worker; io_uring needs a build with liburing. It speeds sequential scans, bitmap heap scans, and vacuum. |
| Virtual generated columns | GENERATED ALWAYS AS (expr) is virtual by default. A virtual column cannot be indexed or logically replicated; write STORED when you need either. |
EXPLAIN ANALYZE | Reports BUFFERS without being asked. See postgres-explain. |
pg_upgrade | Keeps planner statistics, but not extended statistics. Afterwards run vacuumdb --all --analyze-in-stages --missing-stats-only, then vacuumdb --all --analyze-only. |
Choose primary keys that insert in order
Use bigint GENERATED ALWAYS AS IDENTITY when one database assigns IDs. Use uuidv7() when clients or services generate IDs, or when IDs leave the system. Avoid random UUIDv4 (gen_random_uuid()) as a primary key: inserts land across the whole B-tree, causing page splits and poor cache locality. Before 18, generate UUIDv7 in the application.
Keep one source of truth per fact
users.email lives on users, not copied onto orders.
- Start with 3NF. Use foreign keys with explicit
ON DELETE, not application-layer cascades. - Denormalize only for a measured read pattern, and document the job that keeps the copy in sync.
- Use
CHECK,NOT NULL, and domain types. Do not push all validation into fastapi or prisma. - Take locks in a consistent order across transactions; see deadlock.
Pool connections in transaction mode
Postgres runs one process per connection, so hundreds of application connections starve the server. Put PgBouncer in front of any service with more than a handful of workers.
- Use transaction pooling for web workloads. Use session pooling only when clients need session state:
LISTEN/NOTIFY, session-level advisory locks, orSETwithoutLOCAL. - PgBouncer 1.21 and later supports protocol-level prepared statements in transaction mode when
max_prepared_statementsis above zero. - See prisma-pooling for the Prisma settings and supabase for Supavisor.
Send each topic to its page
- Indexes: postgres-indexes. JSONB: postgres-jsonb. Full-text search: postgres-full-text-search.
- Slow queries: postgres-explain, postgres-explain, debug-postgres-slow-query, postgres-window-functions.
- Maintenance and scale: postgres-vacuum, postgres-partitioning, postgres-replication.
- Schema changes: migrations. Row-level security on Supabase: supabase-rls.