01

Storage and mutations

Postgres stores data in tables with a fixed schema. Foreign keys connect one table to another, and the database enforces them. Writes that violate the FK get rejected.

users
0 rows
idPK name email
posts
0 rows
idPK user_idFK title
02

Access patterns · good vs bad

Index lookups over declared keys are fast. Queries that scan every row are slow.

Indexed lookup
SELECT * FROM posts WHERE id = 3
0 / 6 rows checked O(log N)
Full table scan
SELECT * FROM posts WHERE title LIKE '%kernel%'
0 / 6 rows checked O(N)
03

Under load

Connections are finite. Row locks serialize concurrent writes to the same row.

Postgres · pool + queue idle
connection pool (10 slots)
request queue 0
🔒 users.id=1 · row lock held
ops / sec
0
p99 wait + service
0ms
pool utilization
0%
04

When to pick it

If your data has relationships and correctness matters more than throughput, start here.

Use when
  • Data has real relationships (orders, users, line items)
  • You need ACID transactions across rows
  • Queries are varied and exploratory. You don't know them all up front
  • Correctness matters more than last-mile throughput
  • The working set fits on one beefy node
Avoid when
  • You need sub-millisecond key lookups at huge QPS (use KV)
  • You need to scale writes past what one node can do (use wide-column)
  • Rows will be constantly UPDATEd by many writers (lock contention)
  • Data is sparse and each row has totally different columns
  • You want queue-like append-only semantics (use a queue)
Real-world
  • Postgres: the safe default for most products
  • MySQL: same shape, different ecosystem
  • SQLite: embedded, single-file, very capable
  • CockroachDB or Spanner: distributed SQL that scales
  • Typical use: billing, user accounts, CMS, orders