Skip to main content

SQL and Transactions

advanced30 min readLesson 152 of 169

Declarative queries the planner can optimize, joins that model the questions, and transactions whose isolation you can actually reason about.

SQL is declarative โ€” use that

You state what; the planner decides how. Write queries the planner can reason about:

  • Filter with WHERE, aggregate with GROUP BY, and let indexes do the seeking.
  • JOIN ... ON states the relationship explicitly. INNER JOIN keeps matches; LEFT JOIN keeps every left row (and is how you find the missing โ€” users with no orders are LEFT JOIN orders ON ... WHERE orders.id IS NULL).
  • ORDER BY without an index means a sort โ€” of the whole result set, after all the joins. The LIMIT doesn't save you.

Indexes: the planner's shortcuts

An index is a sorted side-structure (typically a B-tree) that turns a scan into a seek. Rules that survive contact with production:

  1. Index the columns you filter, join, and sort on โ€” in the order of the query (a composite index (user_id, created_at) serves WHERE user_id = ? ORDER BY created_at DESC).
  2. Leftmost prefix: the index on (a, b) serves queries on a, and on a AND b โ€” but not on b alone.
  3. Low-selectivity columns barely help (an index on active BOOLEAN in a table where 99% are true seeks almost nothing).
  4. Every index taxes every write. Indexes are not free; each one must be updated on INSERT/UPDATE/DELETE.

Transactions and the classic anomalies

A transaction is all-or-nothing (atomicity) plus isolation โ€” and isolation has levels, because full isolation is expensive:

| Isolation | Anomaly possible | |---|---| | READ UNCOMMITTED | dirty reads (see uncommitted data) | | READ COMMITTED | non-repeatable reads (same query, different answer) | | REPEATABLE READ | phantom rows (new rows appear between reads) | | SERIALIZABLE | none โ€” but the most locking/retrying |

Default for Postgres is READ COMMITTED โ€” which means write-write races are your problem. The fix is optimistic concurrency: stamp rows with a version, and update with UPDATE ... SET v = v + 1 WHERE id = :id AND v = :seen. Zero rows updated means someone raced you: re-read, re-decide, retry. Locking after the fact beats locking in advance for low-contention data.

The N+1 query problem

Fetch 50 orders, then loop and fetch each order's customer: 1 + 50 queries. The network round-trip, not the database work, dominates. Fix: fetch the collection once (WHERE user_id IN (...)) and join in memory โ€” or a single JOIN. Detect it by counting queries per request, which is exactly what the practice below does.

Now practice

Transaction & Pool DrillsOptimistic concurrency with version checks, and a pool that enforces its own capacity.2 challenges ยท ยท ~24 min