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 @.gemini/skills/database-design-principles/SKILL.md.
Query Patterns
CTEs over subqueries for readability:
-- ✅ 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 active_tasks at ON u.id = at.user_id
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:
SELECT title, priority,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM tasks;
Explicit JOIN syntax — never implicit joins in WHERE.
Parameterized queries — never string concatenation. (See GEMINI.md § Security Mandate.)
Migration Safety
For migration strategy (additive-first, two-phase drops, reversibility), see @.gemini/skills/database-design-principles/SKILL.md § Migrations.
- Index creation:
CONCURRENTLY on PostgreSQL for zero-downtime.
- Idempotent DDL — use
IF NOT EXISTS for 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
=)
Composite indexes — column order matters:
-- 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:
-- Index only active rows (5-20x smaller)
CREATE INDEX idx_users_active_email ON users (email) WHERE deleted_at IS NULL;
Covering indexes to avoid heap fetches:
-- INCLUDE non-searchable columns for index-only scans
CREATE INDEX idx_orders_status 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 >> hit in Buffers = data not cached
- Sort Method: external merge =
work_mem too low
Avoid SELECT * — list specific columns.
Keyset pagination over OFFSET for large datasets:
-- O(1) regardless of page depth
SELECT * FROM products WHERE (created_at, id) > ($1, $2)
ORDER BY created_at, id LIMIT 20;
Use LIMIT/OFFSET only for small, bounded result sets.
Concurrency & Locking
Prevent deadlocks — consistent lock ordering:
-- 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:
-- 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:
SELECT pg_advisory_xact_lock(hashtext('daily_report')); -- Released on COMMIT
statement_timeout — always set per-session to prevent runaway queries.
Data Operations
UPSERT — atomic insert-or-update (no race conditions):
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 COPY over 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 ANALYZE after large data changes. Tune autovacuum_vacuum_scale_factor for high-churn tables.
Advanced PostgreSQL
- Full-text search: use
tsvector + GIN index, not LIKE '%term%'.
- JSONB indexing: GIN (
jsonb_path_ops for @> only — 2-3x smaller), expression indexes for key lookups.
Naming
Follow conventions in @.gemini/skills/database-design-principles/SKILL.md § Schema (Naming).
Anti-Patterns
- ❌ Missing indexes on foreign keys
- ❌ N+1 queries (use
JOIN or batch)
- ❌ String concatenation in queries (SQL injection risk)
- ❌ Storing comma-separated values in a single column
- ❌
OFFSET pagination on large datasets (use keyset)
- ❌
timestamp without timezone (use timestamptz)
- ❌
varchar(n) without reason (use text)
- ❌ Random UUID v4 as primary key on large tables (index fragmentation)
- ❌ Check-then-insert pattern (race condition — use UPSERT)
Related
- Database Design Principles @.gemini/skills/database-design-principles/SKILL.md
- Security Principles GEMINI.md § Security Principles
- Performance Optimization Principles @.gemini/skills/performance-optimization-principles/SKILL.md
1---2name: sql-idioms3description: SQL Idioms and Patterns4---56## SQL Idioms and Patterns78SQL rewards set-based thinking, explicit joins, and query plan awareness. Idiomatic SQL = readable, performant, migration-safe.910> Scope: SQL coding idioms. For database design principles, load `@.gemini/skills/database-design-principles/SKILL.md`.1112### Query Patterns13141. **CTEs over subqueries for readability:**15 ```sql16 -- ✅ CTE — readable, debuggable17 WITH active_tasks AS (18 SELECT id, title, priority, user_id19 FROM tasks20 WHERE status = 'active'21 )22 SELECT u.name, COUNT(at.id) AS task_count23 FROM users u24 JOIN active_tasks at ON u.id = at.user_id25 GROUP BY u.name;2627 -- ❌ Nested subquery — hard to read28 SELECT u.name, (SELECT COUNT(*) FROM tasks t WHERE t.user_id = u.id AND t.status = 'active')29 FROM users u;30 ```31322. **Window functions for ranking, running totals:**33 ```sql34 SELECT title, priority,35 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn36 FROM tasks;37 ```38393. **Explicit `JOIN` syntax — never implicit joins in `WHERE`.**40414. **Parameterized queries — never string concatenation.** (See GEMINI.md § Security Mandate.)4243### Migration Safety4445For migration strategy (additive-first, two-phase drops, reversibility), see `@.gemini/skills/database-design-principles/SKILL.md` § Migrations.46471. **Index creation: `CONCURRENTLY`** on PostgreSQL for zero-downtime.482. **Idempotent DDL** — use `IF NOT EXISTS` for tables/indexes; `DO $$ ... pg_constraint check ... $$` for constraints.4950### Index Strategy51521. **Choose the right index type:**53 - B-tree (default): `=`, `<`, `>`, `BETWEEN`, `IN`, `IS NULL`54 - GIN: arrays, JSONB (`@>`), full-text search (`@@`)55 - GiST: geometric data, range types, nearest-neighbor (KNN)56 - BRIN: large time-series tables (10-100x smaller than B-tree)57 - Hash: equality-only (marginally faster than B-tree for `=`)58592. **Composite indexes — column order matters:**60 ```sql61 -- Equality columns first, range columns last (leftmost prefix rule)62 CREATE INDEX idx ON orders (status, created_at);63 -- Works for: WHERE status = 'pending'64 -- Works for: WHERE status = 'pending' AND created_at > '2024-01-01'65 -- Does NOT work for: WHERE created_at > '2024-01-01' (alone)66 ```67683. **Partial indexes for filtered queries:**69 ```sql70 -- Index only active rows (5-20x smaller)71 CREATE INDEX idx_users_active_email ON users (email) WHERE deleted_at IS NULL;72 ```73744. **Covering indexes to avoid heap fetches:**75 ```sql76 -- INCLUDE non-searchable columns for index-only scans77 CREATE INDEX idx_orders_status ON orders (status) INCLUDE (customer_id, total);78 ```79805. **Indexes on foreign keys** — always. PostgreSQL does not auto-index FKs.8182### Performance83841. **`EXPLAIN (ANALYZE, BUFFERS)`** before optimizing — never guess.85 - Seq Scan on large table = missing index86 - Rows Removed by Filter = poor selectivity87 - `read >> hit` in Buffers = data not cached88 - Sort Method: external merge = `work_mem` too low89902. **Avoid `SELECT *`** — list specific columns.91923. **Keyset pagination** over OFFSET for large datasets:93 ```sql94 -- O(1) regardless of page depth95 SELECT * FROM products WHERE (created_at, id) > ($1, $2)96 ORDER BY created_at, id LIMIT 20;97 ```98 Use `LIMIT/OFFSET` only for small, bounded result sets.99100### Concurrency & Locking1011021. **Prevent deadlocks — consistent lock ordering:**103 ```sql104 -- Acquire locks in PK order before updating105 SELECT * FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;106 ```1071082. **SKIP LOCKED for queue processing:**109 ```sql110 -- Workers skip locked rows instead of blocking (10x throughput)111 UPDATE jobs SET status = 'processing'112 WHERE id = (113 SELECT id FROM jobs WHERE status = 'pending'114 ORDER BY created_at LIMIT 1 FOR UPDATE SKIP LOCKED115 ) RETURNING *;116 ```1171183. **Advisory locks for application-level coordination:**119 ```sql120 SELECT pg_advisory_xact_lock(hashtext('daily_report')); -- Released on COMMIT121 ```1221234. **`statement_timeout`** — always set per-session to prevent runaway queries.124125### Data Operations1261271. **UPSERT — atomic insert-or-update (no race conditions):**128 ```sql129 INSERT INTO settings (user_id, key, value) VALUES ($1, $2, $3)130 ON CONFLICT (user_id, key)131 DO UPDATE SET value = EXCLUDED.value, updated_at = now();132 ```1331342. **Bulk loading — use `COPY` over batch INSERTs for large imports.**1351363. **Batch inserts** — multiple rows per statement, not one INSERT per row.137138### Diagnostics1391401. **`pg_stat_statements`** — enable to identify top resource-consuming queries by total time and call frequency.1411422. **VACUUM/ANALYZE** — run `ANALYZE` after large data changes. Tune `autovacuum_vacuum_scale_factor` for high-churn tables.143144### Advanced PostgreSQL1451461. **Full-text search:** use `tsvector` + GIN index, not `LIKE '%term%'`.1472. **JSONB indexing:** GIN (`jsonb_path_ops` for `@>` only — 2-3x smaller), expression indexes for key lookups.148149### Naming150151Follow conventions in `@.gemini/skills/database-design-principles/SKILL.md` § Schema (Naming).152153### Anti-Patterns154155- ❌ Missing indexes on foreign keys156- ❌ N+1 queries (use `JOIN` or batch)157- ❌ String concatenation in queries (SQL injection risk)158- ❌ Storing comma-separated values in a single column159- ❌ `OFFSET` pagination on large datasets (use keyset)160- ❌ `timestamp` without timezone (use `timestamptz`)161- ❌ `varchar(n)` without reason (use `text`)162- ❌ Random UUID v4 as primary key on large tables (index fragmentation)163- ❌ Check-then-insert pattern (race condition — use UPSERT)164165### Related166- Database Design Principles @.gemini/skills/database-design-principles/SKILL.md167- Security Principles GEMINI.md § Security Principles168- Performance Optimization Principles @.gemini/skills/performance-optimization-principles/SKILL.md