Technology · Relational database
PostgreSQL
The default relational database. Transactions and MVCC, indexes worth knowing, how far one node goes, and the ways to scale it.
11 min read
PostgreSQL is a safe default for most systems' primary data. It gives you transactions, constraints, flexible queries, JSON, full-text search and a huge ecosystem. In an interview, picking Postgres needs little justification; picking something else should come with a reason.
When to reach for it
- Data with relationships and invariants: users, orders, payments, inventory, bookings.
- You need transactions across several rows or tables.
- Query patterns will change, so you want ad hoc SQL and secondary indexes.
- Volume fits a primary plus replicas, or a modest number of shards. That covers most products.
How it works
- Every write goes to the write-ahead log (WAL) first, then to data pages. The WAL makes commits durable and drives replication.
- MVCC: an update writes a new row version. Readers see a consistent snapshot without blocking writers; VACUUM later removes dead versions.
- Default isolation is Read Committed. Repeatable Read gives snapshot isolation, and Serializable detects anomalies and aborts one transaction, which the app must retry.
- Indexes are separate structures pointing at rows. B-tree by default.
Indexes worth knowing
B-tree
- Equality, ranges, sorting
- Composite and covering (INCLUDE)
- Every index slows writes
Pick it when: The default for almost everything.
GIN
- Many values per row
- Slower to update
Pick it when: JSONB containment, arrays, full-text search.
GiST / SP-GiST
- Geometric and range types
- Specialized
Pick it when: PostGIS location queries, exclusion constraints on time ranges.
Partial
- Small, targeted
- Only matches queries with the same condition
Pick it when: Index only active rows: WHERE status = 'open'.
How far one node goes
- Simple indexed readsmore from replicas and caches
- tens of thousands per second
- Writesdepends on row size and indexes
- ~5,000–20,000 transactions per second
- Comfortable data sizelarger works, with care
- single-digit TB
- Connectionsuse a pooler like PgBouncer
- hundreds
Scaling it
- Tune first: right indexes, fix N+1 queries, connection pooling.
- Cache hot reads in Redis or at the CDN.
- Read replicas for read-heavy traffic, routing read-your-writes flows to the primary.
- Partition tables (declarative partitioning) by time or tenant to keep indexes small and drop old data cheaply.
- Shard across clusters by a key such as tenant id, with Citus or application-level routing, when one primary can't take the writes.
Where it bites
- Long transactions hold back VACUUM, bloating tables, and can hold locks that queue up other writers.
- Schema changes on big tables can lock them. Add columns without defaults that rewrite, build indexes
CONCURRENTLY, and backfill in batches. - Hot rows (one counter, one popular item) serialize writers on a row lock. See the contention pattern.
- Failover with async replicas can lose the last moments of writes.
Try it
Event Ticketing (Ticketmaster) HLD
Sell seats without double-booking, with Postgres at the core.