SQL
Purpose
Write SQL that the planner can execute efficiently, and read execution plans well enough to know why it did not.
When to Use
- Writing non-trivial queries: aggregations, window functions, recursive CTEs.
- Diagnosing a slow query.
- Designing indexes for a known access pattern.
- Choosing a transaction isolation level.
- Reviewing migrations for lock risk.
Capabilities
- Query authoring: joins, CTEs, window functions, lateral joins, upserts.
- Index design: composite ordering, covering indexes, partial indexes.
- Plan reading:
EXPLAIN (ANALYZE, BUFFERS) and the shapes that signal trouble.
- Isolation levels and the anomalies each one permits.
- Safe migrations: concurrent index builds, backfills, lock avoidance.
Inputs
- The query, the schema, and the row counts of the tables involved.
- Existing indexes.
- The actual execution plan, not a guess about it.
Outputs
- A rewritten query, an index, or both — with a before/after plan.
- Migration statements that do not hold long locks.
Workflow
- Get the plan —
EXPLAIN (ANALYZE, BUFFERS). Never optimize a query you have not profiled.
- Find the expensive node — Look for sequential scans on large tables, nested loops with high row counts, and estimates that diverge from actuals by an order of magnitude.
- Fix the cause — Bad estimate means stale statistics. Sequential scan on a selective filter means a missing index. High row counts through a join means the filter is applied too late.
- Index deliberately — Column order in a composite index is equality columns first, then the range or sort column.
- Re-measure — Confirm with a fresh plan, and check that write throughput did not regress.
Best Practices
- An index on
(a, b) serves queries filtering on a, and on a and b — but not on b alone.
- Wrapping an indexed column in a function (
WHERE lower(email) = ...) disables the index unless the index is on the expression.
SELECT * in application code prevents index-only scans and breaks when the schema changes.
- Never run an unbounded
UPDATE or DELETE on a large table in one transaction — batch it.
CREATE INDEX CONCURRENTLY in production; the plain form locks writes for the duration.
- Prefer keyset pagination (
WHERE id > :last) over OFFSET — offset cost grows linearly with page depth.
Examples
Window function instead of a correlated subquery:
-- Latest order per customer, one pass.
SELECT customer_id, order_id, placed_at, total_cents
FROM (
SELECT
o.customer_id,
o.id AS order_id,
o.placed_at,
o.total_cents,
ROW_NUMBER() OVER (
PARTITION BY o.customer_id
ORDER BY o.placed_at DESC
) AS rn
FROM orders o
WHERE o.placed_at >= now() - interval '90 days'
) ranked
WHERE rn = 1;
Index matching the access pattern:
-- Query: WHERE tenant_id = $1 AND status = 'open' ORDER BY created_at DESC LIMIT 50
CREATE INDEX CONCURRENTLY idx_tickets_tenant_open_recent
ON tickets (tenant_id, status, created_at DESC)
WHERE deleted_at IS NULL;
Notes
- The partial index above only covers live rows, keeping it small and hot in cache.
READ COMMITTED (the default in PostgreSQL) permits non-repeatable reads. If a transaction reads a row, decides, and writes based on that decision, you need REPEATABLE READ plus retry logic, or SELECT ... FOR UPDATE.
- Statistics drift after bulk loads. Run
ANALYZE before benchmarking anything.
1---2name: sql3description: Use when writing or optimizing SQL. Covers query planning, indexing strategy, window functions, CTEs, transaction isolation, and reading EXPLAIN output.4---56# SQL78## Purpose910Write SQL that the planner can execute efficiently, and read execution plans well enough to know why it did not.1112## When to Use1314- Writing non-trivial queries: aggregations, window functions, recursive CTEs.15- Diagnosing a slow query.16- Designing indexes for a known access pattern.17- Choosing a transaction isolation level.18- Reviewing migrations for lock risk.1920## Capabilities2122- Query authoring: joins, CTEs, window functions, lateral joins, upserts.23- Index design: composite ordering, covering indexes, partial indexes.24- Plan reading: `EXPLAIN (ANALYZE, BUFFERS)` and the shapes that signal trouble.25- Isolation levels and the anomalies each one permits.26- Safe migrations: concurrent index builds, backfills, lock avoidance.2728## Inputs2930- The query, the schema, and the row counts of the tables involved.31- Existing indexes.32- The actual execution plan, not a guess about it.3334## Outputs3536- A rewritten query, an index, or both — with a before/after plan.37- Migration statements that do not hold long locks.3839## Workflow40411. **Get the plan** — `EXPLAIN (ANALYZE, BUFFERS)`. Never optimize a query you have not profiled.422. **Find the expensive node** — Look for sequential scans on large tables, nested loops with high row counts, and estimates that diverge from actuals by an order of magnitude.433. **Fix the cause** — Bad estimate means stale statistics. Sequential scan on a selective filter means a missing index. High row counts through a join means the filter is applied too late.444. **Index deliberately** — Column order in a composite index is equality columns first, then the range or sort column.455. **Re-measure** — Confirm with a fresh plan, and check that write throughput did not regress.4647## Best Practices4849- An index on `(a, b)` serves queries filtering on `a`, and on `a` and `b` — but not on `b` alone.50- Wrapping an indexed column in a function (`WHERE lower(email) = ...`) disables the index unless the index is on the expression.51- `SELECT *` in application code prevents index-only scans and breaks when the schema changes.52- Never run an unbounded `UPDATE` or `DELETE` on a large table in one transaction — batch it.53- `CREATE INDEX CONCURRENTLY` in production; the plain form locks writes for the duration.54- Prefer keyset pagination (`WHERE id > :last`) over `OFFSET` — offset cost grows linearly with page depth.5556## Examples5758**Window function instead of a correlated subquery:**5960```sql61-- Latest order per customer, one pass.62SELECT customer_id, order_id, placed_at, total_cents63FROM (64 SELECT65 o.customer_id,66 o.id AS order_id,67 o.placed_at,68 o.total_cents,69 ROW_NUMBER() OVER (70 PARTITION BY o.customer_id71 ORDER BY o.placed_at DESC72 ) AS rn73 FROM orders o74 WHERE o.placed_at >= now() - interval '90 days'75) ranked76WHERE rn = 1;77```7879**Index matching the access pattern:**8081```sql82-- Query: WHERE tenant_id = $1 AND status = 'open' ORDER BY created_at DESC LIMIT 5083CREATE INDEX CONCURRENTLY idx_tickets_tenant_open_recent84 ON tickets (tenant_id, status, created_at DESC)85 WHERE deleted_at IS NULL;86```8788## Notes8990- The partial index above only covers live rows, keeping it small and hot in cache.91- `READ COMMITTED` (the default in PostgreSQL) permits non-repeatable reads. If a transaction reads a row, decides, and writes based on that decision, you need `REPEATABLE READ` plus retry logic, or `SELECT ... FOR UPDATE`.92- Statistics drift after bulk loads. Run `ANALYZE` before benchmarking anything.