irahardianto / sql-idioms
SQL coding standards: CTEs, explicit JOINs, index optimization, query execution plan analysis, transaction locking, and zero-downtime migration patterns. Use when writing complex queries, analyzing performance, or drafting database migrations.
SQL Idioms and Patterns
SQL rewards set-based thinking, explicit joins, and query plan awareness. Idiomatic SQL = readable, performant, migration-safe.
Scope: SQL coding idioms. For database design principles, load
@.agents/rules/database-design-principles.md.
Query Patterns
CTEs over subqueries for readability:
`sql
-- ✅ CTE — readable, debuggable
WITH active_tasks AS (
SELECT id, title, priority, user_id
FROM tasks
WHERE status = 'active'
)
SELECT u.name, COUNT(at.id) AS task_count
FROM users u
JOIN activetasks at ON u.id = at.userid
GROUP BY u.name;-- ❌ Nested subquery — hard to read
SELECT u.name, (SELECT COUNT(*) FROM tasks t WHERE t.user_id = u.id AND t.status = 'active')
FROM users u;`Window functions for ranking, running totals:
`sql
SELECT title, priority,
ROWNUMBER() OVER (PARTITION BY userid ORDER BY created_at DESC) AS rn
FROM tasks;`- Explicit
JOINsyntax — never implicit joins inWHERE. - Parameterized queries — never string concatenation. (See .agents/rules/security-mandate.md.)
Migration Safety
For migration strategy (additive-first, two-phase drops, reversibility), see @.agents/rules/database-design-principles.md § Migrations.
- Index creation:
CONCURRENTLYon PostgreSQL for zero-downtime. - Idempotent DDL — use
IF NOT EXISTSfor tables/indexes;DO $$ ... pg_constraint check ... $$for constraints.
Index Strategy
Choose the right index type:
- B-tree (default):
=,<,>,BETWEEN,IN,IS NULL - GIN: arrays, JSONB (
@>), full-text search (@@) - GiST: geometric data, range types, nearest-neighbor (KNN)
- BRIN: large time-series tables (10-100x smaller than B-tree)
- Hash: equality-only (marginally faster than B-tree for
=)
- B-tree (default):
Composite indexes — column order matters:
`sql
-- Equality columns first, range columns last (leftmost prefix rule)
CREATE INDEX idx ON orders (status, created_at);
-- Works for: WHERE status = 'pending'
-- Works for: WHERE status = 'pending' AND created_at > '2024-01-01'
-- Does NOT work for: WHERE created_at > '2024-01-01' (alone)`Partial indexes for filtered queries:
`sql
-- Index only active rows (5-20x smaller)
CREATE INDEX idxusersactiveemail ON users (email) WHERE deletedat IS NULL;`Covering indexes to avoid heap fetches:
`sql
-- INCLUDE non-searchable columns for index-only scans
CREATE INDEX idxordersstatus ON orders (status) INCLUDE (customer_id, total);`- Indexes on foreign keys — always. PostgreSQL does not auto-index FKs.
Performance
EXPLAIN (ANALYZE, BUFFERS)before optimizing — never guess.- Seq Scan on large table = missing index
- Rows Removed by Filter = poor selectivity
read >> hitin Buffers = data not cached- Sort Method: external merge =
work_memtoo low
- Avoid
SELECT *— list specific columns. Keyset pagination over OFFSET for large datasets:
`sql
-- O(1) regardless of page depth
SELECT * FROM products WHERE (created_at, id) > ($1, $2)
ORDER BY created_at, id LIMIT 20;`
UseLIMIT/OFFSETonly for small, bounded result sets.
Concurrency & Locking
Prevent deadlocks — consistent lock ordering:
`sql
-- Acquire locks in PK order before updating
SELECT * FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;`SKIP LOCKED for queue processing:
`sql
-- Workers skip locked rows instead of blocking (10x throughput)
UPDATE jobs SET status = 'processing'
WHERE id = (
SELECT id FROM jobs WHERE status = 'pending'
ORDER BY created_at LIMIT 1 FOR UPDATE SKIP LOCKED
) RETURNING *;`Advisory locks for application-level coordination:
`sql
SELECT pgadvisoryxactlock(hashtext('dailyreport')); -- Released on COMMIT`statement_timeout— always set per-session to prevent runaway queries.
Data Operations
UPSERT — atomic insert-or-update (no race conditions):
`sql
INSERT INTO settings (user_id, key, value) VALUES ($1, $2, $3)
ON CONFLICT (user_id, key)
DO UPDATE SET value = EXCLUDED.value, updated_at = now();`- Bulk loading — use
COPYover batch INSERTs for large imports. - Batch inserts — multiple rows per statement, not one INSERT per row.
Diagnostics
pg_stat_statements— enable to identify top resource-consuming queries by total time and call frequency.- VACUUM/ANALYZE — run
ANALYZEafter large data changes. Tuneautovacuum_vacuum_scale_factorfor high-churn tables.
Advanced PostgreSQL
- Full-text search: use
tsvector+ GIN index, notLIKE '%term%'. - JSONB indexing: GIN (
jsonb_path_opsfor@>only — 2-3x smaller), expression indexes for key lookups.
Naming
Follow conventions in @.agents/rules/database-design-principles.md § Schema (Naming).
Anti-Patterns
- ❌ Missing indexes on foreign keys
- ❌ N+1 queries (use
JOINor batch) - ❌ String concatenation in queries (SQL injection risk)
- ❌ Storing comma-separated values in a single column
- ❌
OFFSETpagination on large datasets (use keyset) - ❌
timestampwithout timezone (usetimestamptz) - ❌
varchar(n)without reason (usetext) - ❌ Random UUID v4 as primary key on large tables (index fragmentation)
- ❌ Check-then-insert pattern (race condition — use UPSERT)
Related
- Database Design Principles @.agents/rules/database-design-principles.md
- Security Principles .agents/rules/security-principles.md
- Performance Optimization Principles @.agents/rules/performance-optimization-principles.md