Salman Adnan

postgres-scaling-lab

Postgres Scaling Lab

When the database behind a program feels slow, the tempting move is to buy a different kind of database; this studies how far the one you already have can go. Adding an index, the same idea as a book's index, cut a common lookup from reading all 2,494 blocks of the table to reading 4. Splitting one table up by date, reported honestly, made another lookup about 70 percent slower.

Solo, on course scaffolding

Built solo by Salman Adnan for an Enterprise Software Development course against course-provided scaffolding; his work is the solution SQL, the replication setup, the routing app, and the analysis. Docker was unavailable on the latest machine, so the numbers come from the original course Docker runs.

Shard and Stream: one large table splits into smaller pieces, and a running journal of every change streams out to copies of the database, which catch up behind it. Those are the two scaling moves this project actually measures. Live and interactive: drag it to orbit, scroll or pinch to zoom. Open full screen
Real terminal output of EXPLAIN ANALYZE before and after adding an index, on a live local Postgres cluster: a sequential scan reading 2,494 blocks becomes a bitmap index scan reading 4.
A scratch local PostgreSQL 18.4 cluster running this repo's own schema (100k users, 500k orders) and its idx_users_email index. The email lookup stops reading all 2,494 blocks of the table and reads 4: a sequential scan that threw away 99,999 of 100,000 rows becomes a bitmap index scan. Warm, that is 18.8 ms to 0.106 ms. The block counts are identical on every run; the timings are not, so the blocks are the honest headline. Full stdout, including the repeat runs, is saved beside the image in docs/screenshots/run-output.txt. The 35 ms to 0.5 ms figure quoted below is the same query on the original course Docker setup; different machine, same fix.
35 -> 0.5 msemail match after indexing
9,154 -> 975buffer blocks after pruning
5 / 5replica routing tests

Overview

A three-part study of scaling a single PostgreSQL node before reaching for NoSQL: find slow queries and index them, partition a time-series table, then add streaming read replicas with read and write routing in the application.

Key features

  • Part 1, indexing: 100k users and 500k orders, using the slow-query log and auto_explain to find offenders, then adding B-tree, text_pattern_ops, GIN pg_trgm, and GIN JSONB indexes.
  • Part 2, range partitioning: 600k events, with monthly partitions for 2024 plus weekly partitions for hot data and partition pruning.
  • Part 3, read replicas: one primary and two async streaming replicas via pg_basebackup and replication slots, with a psycopg app routing writes to the primary and reads to a random replica.
  • Each part is an independent Docker Compose stack.

Results

  • Indexing before and after: email exact match 35 ms to 0.5 ms, email prefix 125 ms to 3 ms, amount range 3,100 ms to 1,300 ms, JSONB containment 780 ms to 280 ms.
  • Partitioning: monthly analysis dropped from 9,154 buffer blocks to 975 (pruning skips 11 months), and recent-events improved 48%.
  • An honest negative: user-activity queries got about 70% slower after partitioning, because they filter on user_id, not the partition key.
  • Part 3: the test suite passes 5 of 5, confirming writes hit the primary and reads hit the replicas.

Tech stack

  • PostgreSQL 17 (measured) / 18 (verified)
  • Docker Compose
  • Python with psycopg
  • pytest

Book a call

Let's talk about what you're building.

Pick a slot below.