PostgreSQL Advanced Best Practices (PostgreSQL 18+)
Architecture at a Glance
┌─── PostgreSQL Database ──────────────────────────────┐
│ │
│ ┌──────────────────┐ ┌───────────────────────┐ │
│ │ api schema │ │ private schema │ │
┌─────────────┐ │ │──────────────────│ │───────────────────────│ │
│ Application │─EXECUTE─▶│ get_customer() │───▶│ set_updated_at() │ │
└─────────────┘ │ │ insert_order() │ │ hash_password() │ │
│ │ └────────┬─────────┘ └──────────┬────────────┘ │
│ │ │ │ │
│ │ │ SECURITY DEFINER │ triggers │
│ │ ▼ ▼ │
│ │ ┌──────────────────────────────────────────────┐ │
│ │ │ data schema │ │
BLOCKED │ │──────────────────────────────────────────────│ │
│ │ │ customers orders ... │ │
└ ─ ─ ─ ✕ │ └──────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘
Skill Contents
🚀 Getting Started (Read These First)
| Document |
Purpose |
| quick-reference.md |
QUICK LOOKUP - Single-page cheat sheet (print this!) |
| schema-architecture.md |
START HERE - Schema separation pattern (data/private/api) |
| coding-standards-trivadis.md |
Coding standards & naming conventions (l_, g_, co_) |
📚 Core Reference (Use Daily)
| Document |
Purpose |
| plpgsql-table-api.md |
Table API functions, procedures, triggers |
| schema-naming.md |
Naming conventions for all objects |
| data-types.md |
Data type selection (UUIDv7, text, timestamptz) |
| indexes-constraints.md |
Index types, strategies, constraints |
| migrations.md |
Native migration system documentation |
| anti-patterns.md |
Common mistakes to avoid |
| checklists-troubleshooting.md |
Project checklists & problem solutions |
🔧 Advanced Topics (When Needed)
| Document |
Purpose |
| testing-patterns.md |
pgTAP unit testing, test factories |
| performance-tuning.md |
EXPLAIN ANALYZE, query optimization, JIT |
| row-level-security.md |
RLS patterns, multi-tenant isolation |
| jsonb-patterns.md |
JSONB indexing, queries, validation |
| audit-logging.md |
Generic audit triggers, change tracking |
| bulk-operations.md |
COPY, batch inserts, upserts |
| session-management.md |
Session variables, connection pooling |
| transaction-patterns.md |
Isolation levels, locking, deadlock prevention |
| full-text-search.md |
tsvector, tsquery, ranking, multi-language |
| partitioning.md |
Range, list, hash partitioning strategies |
| window-functions.md |
Frames, ranking, running calculations |
| time-series.md |
Time-series data patterns, BRIN indexes |
| event-sourcing.md |
Event store, projections, CQRS |
| queue-patterns.md |
Job queues, SKIP LOCKED, LISTEN/NOTIFY |
| encryption.md |
pgcrypto, column encryption, TLS |
| vector-search.md |
pgvector, embeddings, similarity search |
| postgis-patterns.md |
Spatial data, geographic queries |
🚀 DevOps & Migration
| Document |
Purpose |
| oracle-migration-guide.md |
PL/SQL to PL/pgSQL conversion |
| cicd-integration.md |
GitHub Actions, GitLab CI, Docker |
| monitoring-observability.md |
pg_stat_statements, metrics, alerting |
| backup-recovery.md |
pg_dump, pg_basebackup, PITR |
| replication-ha.md |
Streaming/logical replication, failover |
📊 Data Warehousing
| Document |
Purpose |
| data-warehousing-medallion.md |
Medallion Architecture - Bronze/Silver/Gold, data lineage, ETL |
| analytical-queries.md |
Analytical query patterns, OLAP optimization, GROUPING SETS |
Executable Scripts
| Script |
Purpose |
| 001_install_migration_system.sql |
Install migration system (core functions) |
| 002_migration_runner_helpers.sql |
Helper procedures (run_versioned, run_repeatable) |
| 003_example_migrations.sql |
Example migration patterns |
| 999_uninstall_migration_system.sql |
Clean removal of migration system |
Core Architecture
Schema Separation Pattern
Application → api schema → data schema
↓
private schema (triggers, helpers)
| Schema |
Contains |
Access |
Purpose |
data |
Tables, indexes |
None |
Data storage |
private |
Triggers, helpers |
None |
Internal logic |
api |
Functions, procedures |
Applications |
External interface |
app_audit |
Audit tables |
Admins |
Change tracking |
app_migration |
Migration tracking |
Admins |
Schema versioning |
Security Model
All api functions MUST have:
SECURITY DEFINER
SET search_path = data, private, pg_temp
Quick Reference
Create Table Pattern
CREATE TABLE data.{table_name} (
id uuid PRIMARY KEY DEFAULT uuidv7(),
-- columns...
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TRIGGER {table}_bu_updated_trg
BEFORE UPDATE ON data.{table_name}
FOR EACH ROW EXECUTE FUNCTION private.set_updated_at();
API Function Pattern
CREATE FUNCTION api.{action}_{entity}(in_param type)
RETURNS TABLE (col1 type, col2 type)
LANGUAGE sql STABLE
SECURITY DEFINER
SET search_path = data, private, pg_temp
AS $$
SELECT col1, col2 FROM data.{table} WHERE ...;
$$;
API Procedure Pattern
CREATE PROCEDURE api.{action}_{entity}(
in_param type,
INOUT io_id uuid DEFAULT NULL
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = data, private, pg_temp
AS $$
BEGIN
INSERT INTO data.{table} (...) VALUES (...) RETURNING id INTO io_id;
END;
$$;
Migration Pattern
SELECT app_migration.acquire_lock();
CALL app_migration.run_versioned(
in_version := '001',
in_description := 'Description',
in_sql := $mig$ ... $mig$,
in_rollback_sql := '...'
);
SELECT app_migration.release_lock();
Naming Conventions
Trivadis-Style Variable Prefixes
| Prefix |
Type |
Example |
l_ |
Local variable |
l_customer_count |
g_ |
Session/global variable |
g_current_user_id |
co_ |
Constant |
co_max_retries |
in_ |
IN parameter |
in_customer_id |
out_ |
OUT parameter (functions only) |
out_total |
io_ |
INOUT parameter (procedures) |
io_id |
c_ |
Cursor |
c_active_orders |
r_ |
Record |
r_customer |
t_ |
Array/table |
t_order_ids |
e_ |
Exception |
e_not_found |
Note: PostgreSQL procedures only support INOUT parameters, not OUT. Use io_ prefix for all procedure output parameters.
Database Objects
| Object |
Pattern |
Example |
| Table |
snake_case, plural |
orders, order_items |
| Column |
snake_case |
customer_id, created_at |
| Primary Key |
id |
id |
| Foreign Key |
{table_singular}_id |
customer_id |
| Index |
{table}_{cols}_idx |
orders_customer_id_idx |
| Unique |
{table}_{cols}_key |
users_email_key |
| Function |
{action}_{entity} |
get_customer, select_orders |
| Procedure |
{action}_{entity} |
insert_order, update_status |
| Trigger |
{table}_{timing}{event}_trg |
orders_bu_trg |
Data Type Recommendations
| Use |
Instead Of |
text |
char(n), varchar(n) |
numeric(p,s) |
money, float |
timestamptz |
timestamp |
boolean |
integer flags |
uuidv7() |
serial, uuid_generate_v4() |
GENERATED ALWAYS AS IDENTITY |
serial, bigserial |
jsonb |
json, EAV pattern |
Critical Anti-Patterns
- ❌ Direct table access from applications
- ❌
RETURNS SETOF table (exposes all columns)
- ❌ Missing
SET search_path with SECURITY DEFINER
- ❌
timestamp without timezone
- ❌
NOT IN with subqueries (use NOT EXISTS)
- ❌
BETWEEN with timestamps (use >= AND <)
- ❌ Missing indexes on foreign keys
- ❌
serial/bigserial (use IDENTITY)
- ❌
varchar(n) arbitrary limits (use text)
- ❌
SELECT FOR UPDATE without NOWAIT/SKIP LOCKED
PostgreSQL 18+ Features
| Feature |
Usage |
uuidv7() |
id uuid DEFAULT uuidv7() - timestamp-ordered UUIDs |
| Virtual generated columns |
col type GENERATED ALWAYS AS (expr) - computed at query time |
OLD/NEW in RETURNING |
UPDATE ... RETURNING OLD.col, NEW.col |
| Temporal constraints |
PRIMARY KEY (id) WITHOUT OVERLAPS |
NOT VALID constraints |
Add constraints without full table scan |
File Organization
db/
├── migrations/
│ ├── V001__create_schemas.sql
│ ├── V002__create_tables.sql
│ └── repeatable/
│ ├── R__private_triggers.sql
│ └── R__api_functions.sql
├── schemas/
│ ├── data/ # Table definitions
│ ├── private/ # Internal functions
│ └── api/ # External interface
└── seeds/ # Reference data
1---2name: postgresql-best-practices3description: PostgreSQL 18+ comprehensive best practices for enterprise database development. Provides schema architecture patterns, Table API design, PL/pgSQL coding standards, migrations, and data warehousing. USE THIS SKILL WHEN THE USER: - Creates PostgreSQL schemas, tables, functions, procedures, or triggers - Asks about PostgreSQL data types (uuid, text, timestamptz, jsonb, numeric) - Writes PL/pgSQL code and needs naming conventions (l_, in_, io_, co_ prefixes) - Implements Table API pattern (SECURITY DEFINER functions, schema separation) - Sets up database migrations or schema versioning - Needs index optimization, constraint design, or query performance help - Asks about PostgreSQL 18+ features (uuidv7, virtual columns, temporal constraints) - Builds data warehouses with Medallion Architecture (Bronze/Silver/Gold) - Needs data lineage tracking, ETL patterns, or audit logging - Reviews database code for best practices or anti-patterns - Migrates from Oracle PL/SQL to PostgreSQL PL/pgSQL - Sets up CI/CD pipelines f4---56# PostgreSQL Advanced Best Practices (PostgreSQL 18+)78## Architecture at a Glance910```11 ┌─── PostgreSQL Database ──────────────────────────────┐12 │ │13 │ ┌──────────────────┐ ┌───────────────────────┐ │14 │ │ api schema │ │ private schema │ │15 ┌─────────────┐ │ │──────────────────│ │───────────────────────│ │16 │ Application │─EXECUTE─▶│ get_customer() │───▶│ set_updated_at() │ │17 └─────────────┘ │ │ insert_order() │ │ hash_password() │ │18 │ │ └────────┬─────────┘ └──────────┬────────────┘ │19 │ │ │ │ │20 │ │ │ SECURITY DEFINER │ triggers │21 │ │ ▼ ▼ │22 │ │ ┌──────────────────────────────────────────────┐ │23 │ │ │ data schema │ │24 BLOCKED │ │──────────────────────────────────────────────│ │25 │ │ │ customers orders ... │ │26 └ ─ ─ ─ ✕ │ └──────────────────────────────────────────────┘ │27 │ │28 └──────────────────────────────────────────────────────┘29```3031## Skill Contents3233### 🚀 Getting Started (Read These First)3435| Document | Purpose |36|----------|---------|37| [quick-reference.md](references/quick-reference.md) | **QUICK LOOKUP** - Single-page cheat sheet (print this!) |38| [schema-architecture.md](references/schema-architecture.md) | **START HERE** - Schema separation pattern (data/private/api) |39| [coding-standards-trivadis.md](references/coding-standards-trivadis.md) | Coding standards & naming conventions (l_, g_, co_) |4041### 📚 Core Reference (Use Daily)4243| Document | Purpose |44|----------|---------|45| [plpgsql-table-api.md](references/plpgsql-table-api.md) | Table API functions, procedures, triggers |46| [schema-naming.md](references/schema-naming.md) | Naming conventions for all objects |47| [data-types.md](references/data-types.md) | Data type selection (UUIDv7, text, timestamptz) |48| [indexes-constraints.md](references/indexes-constraints.md) | Index types, strategies, constraints |49| [migrations.md](references/migrations.md) | Native migration system documentation |50| [anti-patterns.md](references/anti-patterns.md) | Common mistakes to avoid |51| [checklists-troubleshooting.md](references/checklists-troubleshooting.md) | Project checklists & problem solutions |5253### 🔧 Advanced Topics (When Needed)5455| Document | Purpose |56|----------|---------|57| [testing-patterns.md](references/testing-patterns.md) | pgTAP unit testing, test factories |58| [performance-tuning.md](references/performance-tuning.md) | EXPLAIN ANALYZE, query optimization, JIT |59| [row-level-security.md](references/row-level-security.md) | RLS patterns, multi-tenant isolation |60| [jsonb-patterns.md](references/jsonb-patterns.md) | JSONB indexing, queries, validation |61| [audit-logging.md](references/audit-logging.md) | Generic audit triggers, change tracking |62| [bulk-operations.md](references/bulk-operations.md) | COPY, batch inserts, upserts |63| [session-management.md](references/session-management.md) | Session variables, connection pooling |64| [transaction-patterns.md](references/transaction-patterns.md) | Isolation levels, locking, deadlock prevention |65| [full-text-search.md](references/full-text-search.md) | tsvector, tsquery, ranking, multi-language |66| [partitioning.md](references/partitioning.md) | Range, list, hash partitioning strategies |67| [window-functions.md](references/window-functions.md) | Frames, ranking, running calculations |68| [time-series.md](references/time-series.md) | Time-series data patterns, BRIN indexes |69| [event-sourcing.md](references/event-sourcing.md) | Event store, projections, CQRS |70| [queue-patterns.md](references/queue-patterns.md) | Job queues, SKIP LOCKED, LISTEN/NOTIFY |71| [encryption.md](references/encryption.md) | pgcrypto, column encryption, TLS |72| [vector-search.md](references/vector-search.md) | pgvector, embeddings, similarity search |73| [postgis-patterns.md](references/postgis-patterns.md) | Spatial data, geographic queries |7475### 🚀 DevOps & Migration7677| Document | Purpose |78|----------|---------|79| [oracle-migration-guide.md](references/oracle-migration-guide.md) | PL/SQL to PL/pgSQL conversion |80| [cicd-integration.md](references/cicd-integration.md) | GitHub Actions, GitLab CI, Docker |81| [monitoring-observability.md](references/monitoring-observability.md) | pg_stat_statements, metrics, alerting |82| [backup-recovery.md](references/backup-recovery.md) | pg_dump, pg_basebackup, PITR |83| [replication-ha.md](references/replication-ha.md) | Streaming/logical replication, failover |8485### 📊 Data Warehousing8687| Document | Purpose |88|----------|---------|89| [data-warehousing-medallion.md](references/data-warehousing-medallion.md) | **Medallion Architecture** - Bronze/Silver/Gold, data lineage, ETL |90| [analytical-queries.md](references/analytical-queries.md) | Analytical query patterns, OLAP optimization, GROUPING SETS |9192### Executable Scripts9394| Script | Purpose |95|--------|---------|96| [001_install_migration_system.sql](scripts/001_install_migration_system.sql) | Install migration system (core functions) |97| [002_migration_runner_helpers.sql](scripts/002_migration_runner_helpers.sql) | Helper procedures (`run_versioned`, `run_repeatable`) |98| [003_example_migrations.sql](scripts/003_example_migrations.sql) | Example migration patterns |99| [999_uninstall_migration_system.sql](scripts/999_uninstall_migration_system.sql) | Clean removal of migration system |100101---102103## Core Architecture104105### Schema Separation Pattern106107```108Application → api schema → data schema109 ↓110 private schema (triggers, helpers)111```112113| Schema | Contains | Access | Purpose |114|--------|----------|--------|---------|115| `data` | Tables, indexes | None | Data storage |116| `private` | Triggers, helpers | None | Internal logic |117| `api` | Functions, procedures | Applications | External interface |118| `app_audit` | Audit tables | Admins | Change tracking |119| `app_migration` | Migration tracking | Admins | Schema versioning |120121### Security Model122123All `api` functions MUST have:124```sql125SECURITY DEFINER126SET search_path = data, private, pg_temp127```128129---130131## Quick Reference132133### Create Table Pattern134135```sql136CREATE TABLE data.{table_name} (137 id uuid PRIMARY KEY DEFAULT uuidv7(),138 -- columns...139 created_at timestamptz NOT NULL DEFAULT now(),140 updated_at timestamptz NOT NULL DEFAULT now()141);142143CREATE TRIGGER {table}_bu_updated_trg144 BEFORE UPDATE ON data.{table_name}145 FOR EACH ROW EXECUTE FUNCTION private.set_updated_at();146```147148### API Function Pattern149150```sql151CREATE FUNCTION api.{action}_{entity}(in_param type)152RETURNS TABLE (col1 type, col2 type)153LANGUAGE sql STABLE154SECURITY DEFINER155SET search_path = data, private, pg_temp156AS $$157 SELECT col1, col2 FROM data.{table} WHERE ...;158$$;159```160161### API Procedure Pattern162163```sql164CREATE PROCEDURE api.{action}_{entity}(165 in_param type,166 INOUT io_id uuid DEFAULT NULL167)168LANGUAGE plpgsql169SECURITY DEFINER170SET search_path = data, private, pg_temp171AS $$172BEGIN173 INSERT INTO data.{table} (...) VALUES (...) RETURNING id INTO io_id;174END;175$$;176```177178### Migration Pattern179180```sql181SELECT app_migration.acquire_lock();182183CALL app_migration.run_versioned(184 in_version := '001',185 in_description := 'Description',186 in_sql := $mig$ ... $mig$,187 in_rollback_sql := '...'188);189190SELECT app_migration.release_lock();191```192193---194195## Naming Conventions196197### Trivadis-Style Variable Prefixes198199| Prefix | Type | Example |200|--------|------|---------|201| `l_` | Local variable | `l_customer_count` |202| `g_` | Session/global variable | `g_current_user_id` |203| `co_` | Constant | `co_max_retries` |204| `in_` | IN parameter | `in_customer_id` |205| `out_` | OUT parameter (functions only) | `out_total` |206| `io_` | INOUT parameter (procedures) | `io_id` |207| `c_` | Cursor | `c_active_orders` |208| `r_` | Record | `r_customer` |209| `t_` | Array/table | `t_order_ids` |210| `e_` | Exception | `e_not_found` |211212> **Note**: PostgreSQL procedures only support INOUT parameters, not OUT. Use `io_` prefix for all procedure output parameters.213214### Database Objects215216| Object | Pattern | Example |217|--------|---------|---------|218| Table | `snake_case`, plural | `orders`, `order_items` |219| Column | `snake_case` | `customer_id`, `created_at` |220| Primary Key | `id` | `id` |221| Foreign Key | `{table_singular}_id` | `customer_id` |222| Index | `{table}_{cols}_idx` | `orders_customer_id_idx` |223| Unique | `{table}_{cols}_key` | `users_email_key` |224| Function | `{action}_{entity}` | `get_customer`, `select_orders` |225| Procedure | `{action}_{entity}` | `insert_order`, `update_status` |226| Trigger | `{table}_{timing}{event}_trg` | `orders_bu_trg` |227228---229230## Data Type Recommendations231232| Use | Instead Of |233|-----|------------|234| `text` | `char(n)`, `varchar(n)` |235| `numeric(p,s)` | `money`, `float` |236| `timestamptz` | `timestamp` |237| `boolean` | `integer` flags |238| `uuidv7()` | `serial`, `uuid_generate_v4()` |239| `GENERATED ALWAYS AS IDENTITY` | `serial`, `bigserial` |240| `jsonb` | `json`, EAV pattern |241242---243244## Critical Anti-Patterns2452461. ❌ Direct table access from applications2472. ❌ `RETURNS SETOF table` (exposes all columns)2483. ❌ Missing `SET search_path` with `SECURITY DEFINER`2494. ❌ `timestamp` without timezone2505. ❌ `NOT IN` with subqueries (use `NOT EXISTS`)2516. ❌ `BETWEEN` with timestamps (use `>= AND <`)2527. ❌ Missing indexes on foreign keys2538. ❌ `serial`/`bigserial` (use `IDENTITY`)2549. ❌ `varchar(n)` arbitrary limits (use `text`)25510. ❌ `SELECT FOR UPDATE` without `NOWAIT`/`SKIP LOCKED`256257---258259## PostgreSQL 18+ Features260261| Feature | Usage |262|---------|-------|263| `uuidv7()` | `id uuid DEFAULT uuidv7()` - timestamp-ordered UUIDs |264| Virtual generated columns | `col type GENERATED ALWAYS AS (expr)` - computed at query time |265| `OLD`/`NEW` in RETURNING | `UPDATE ... RETURNING OLD.col, NEW.col` |266| Temporal constraints | `PRIMARY KEY (id) WITHOUT OVERLAPS` |267| `NOT VALID` constraints | Add constraints without full table scan |268269---270271## File Organization272273```274db/275├── migrations/276│ ├── V001__create_schemas.sql277│ ├── V002__create_tables.sql278│ └── repeatable/279│ ├── R__private_triggers.sql280│ └── R__api_functions.sql281├── schemas/282│ ├── data/ # Table definitions283│ ├── private/ # Internal functions284│ └── api/ # External interface285└── seeds/ # Reference data286```