| name | postgres-patterns |
| description | Use when designing schemas, writing migrations, optimizing queries, or reviewing PostgreSQL database work |
PostgreSQL Patterns
Schema Design Principles
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
Indexing Strategy
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);
CREATE INDEX idx_users_active ON users(email) WHERE deleted_at IS NULL;
CREATE INDEX idx_posts_user_status ON posts(user_id, status);
Soft Delete Pattern
ALTER TABLE posts ADD COLUMN deleted_at TIMESTAMPTZ;
CREATE VIEW active_posts AS
SELECT * FROM posts WHERE deleted_at IS NULL;
SELECT * FROM posts WHERE deleted_at IS NULL AND user_id = $1;
Migrations (with timestamps)
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
DROP TABLE IF EXISTS users;
Common Query Patterns
SELECT * FROM posts
WHERE created_at < $1
ORDER BY created_at DESC
LIMIT 20;
INSERT INTO user_settings (user_id, key, value)
VALUES ($1, $2, $3)
ON CONFLICT (user_id, key)
DO UPDATE SET value = EXCLUDED.value, updated_at = NOW();
SELECT
DATE_TRUNC('day', created_at) AS day,
COUNT(*) AS total
FROM events
WHERE created_at >= NOW() - INTERVAL '30 days'
GROUP BY 1
ORDER BY 1;
JSON/JSONB
ALTER TABLE products ADD COLUMN metadata JSONB;
SELECT * FROM products WHERE metadata->>'category' = 'electronics';
CREATE INDEX idx_products_category ON products((metadata->>'category'));
Key Principles
- Use
UUID as primary key — never expose sequential integers to clients
- Always define
NOT NULL unless a column is intentionally nullable
- Index every foreign key column
- Use
TIMESTAMPTZ (not TIMESTAMP) — always store timezone-aware timestamps
- Use keyset pagination over
OFFSET for tables larger than 10k rows
- Avoid
SELECT * in application queries — list columns explicitly
- Run
EXPLAIN ANALYZE before deploying any new query