Module 9 / Growing with your data

Module 9 · lesson 1 of 8 · Read 9 min

Data keeps growing

Three years and 200 million orders later.

For the first year, Tiffin's database was the least interesting part of the system. One Postgres primary, a couple of read replicas, a connection pool, a cache in front of the menus. Every problem in Modules 3 to 8 was about traffic: too many requests at 8pm.

Traffic comes and goes. Data doesn't. Every order Tiffin has ever taken is still in the orders table, and the table never shrinks. This lesson is about what happens when the size of your data, not the number of requests, becomes the problem.

Three years later

Tiffin now runs in a handful of cities and takes around 250,000 orders a day. Add up three years of growth and the orders table has about 200 million rows.

Let's estimate how big that really is. Rough sizes per row, including some overhead:

Table Rows per order Bytes per row Rows after 3 years Size
orders 1 ~500 200 million ~100 GB
order_items ~3 ~200 600 million ~120 GB
order_events (status changes) ~6 ~100 1.2 billion ~120 GB
rider_locations (trip history) ~30 ~60 6 billion ~360 GB

Then add indexes. On a busy table, indexes are often 50% to 100% of the table size again. So the order tables alone are roughly 500 GB to 700 GB, and the rider location history is another 550 GB to 700 GB on its own, as much as all of them together. Add customers, payments, menus, reviews and the rest, and the whole database is about 1.5 TB.

That last row is the surprise. Nobody thinks of rider pings as "the data". Riders send their location every 4 seconds, and even though the history keeps only one point every 30 seconds while an order is on the road (Module 6), a 15-minute ride is 30 rows per order. Small rows, huge count. This is common: the table you think about least often grows the fastest.

Why big hurts, even on a big disk

A modern server can hold 10 TB or more of SSD. So why worry? Because almost everything about running a database gets slower as the data grows, and most of it has nothing to do with disk space.

The working set stops fitting in memory

A database is fast when the data it needs is already in RAM. Say the primary has 256 GB of memory. When the database was 100 GB, every index and most rows lived in memory. At 1.5 TB, only a small slice does.

That's fine as long as queries only touch recent data. A query that wanders into old orders now reads from disk, and each one pushes useful pages out of the cache for everyone else.

The working set is the data your queries touch often enough that it needs to stay in memory. When it no longer fits, latency jumps.

Indexes get deeper and wider

A B-tree index on 200 million rows is still fast for a single lookup: four or five levels deep. But the index itself is now several gigabytes, and every insert has to update it. Five indexes on orders means five index writes per order.

Maintenance becomes a project

These are the jobs that quietly turn from "minutes" into "a weekend":

  • Backups. Copying 1.5 TB at 200 MB/s takes about 2 hours. Restoring takes at least as long, and that's your recovery time (RTO) from Module 5.
  • New replicas. A new read replica has to copy the whole database first. Hours again.
  • Schema changes. Changing a column's type, or building a new index on a 200-million-row table, can take hours and lock or slow writes. (Modern Postgres and MySQL add a plain new column instantly; it's the changes that rewrite or scan every row that hurt.)
  • Cleanup. Postgres's VACUUM cleans up dead rows, and it has to scan the table's changed pages and every one of its indexes to do it. On a huge, busy table it runs for hours and competes with real traffic.
  • Deleting. DELETE FROM orders WHERE created_at < '2023-01-01' on 50 million rows creates 50 million dead rows, a huge write-ahead log and replica lag. It can be worse than doing nothing.

Do the backup maths

Tiffin's database is 1.5 TB. A full restore runs at about 150 MB/s. The business says that if the database is lost, Tiffin must be back within 2 hours. Is it?

Show one answer

1.5 TB is 1,500,000 MB. At 150 MB/s that's 10,000 seconds, about 2 hours 45 minutes, before replaying any logs. So no. The restore alone misses the target.

The cheapest fix is not a faster disk. It is having less data in the database you need to restore quickly. Most of those 1.5 TB are old orders and rider pings that nobody needs in the first 2 hours of an outage. That's what the next lesson is about.

Most data is cold

Look at who actually reads Tiffin's orders:

  • The live order screens read orders from the last few hours.
  • "Your orders" in the customer app reads the last few weeks. Very few people scroll back two years.
  • Restaurant payouts read the last week or month.
  • Support reads orders from the last 30 to 60 days, when complaints come in.
  • Finance and tax need everything, but once a month or once a year, and they're happy to wait.

Put numbers on it and you get a pattern almost every app has: something like 95% of reads touch data from the last 30 days, which is only a few percent of the rows.

Hotlast 30 days, read constantly
→
Warm1 to 18 months, read sometimes
→
Coldolder, read almost never
The same table, three very different access patterns. Treating them the same is what makes big tables expensive.

Data that is read constantly is hot. Data that is kept but rarely read is cold. Most of the pain of a big database comes from keeping cold data in the same place, on the same expensive hardware, with the same indexes, as hot data.

Not every growth problem is the same

Before you reach for a fix, name the problem. "The database is big" can mean very different things:

Symptom What's actually wrong Usual fix
Backups, restores and migrations take hours Too much data in one place Archive cold data
Queries on recent data slow down over time Hot rows mixed with cold rows in one huge table and index Partition the table
Text and location search is slow or poor Wrong kind of index for the question A separate search index
The primary can't keep up with writes, even after tuning Too many writes for one machine Shard
Engineers block each other shipping changes The codebase, not the data, is too big Split the app

Each row of that table is a lesson in this module, and they're in that order on purpose. The early ones are cheap: a batch job, a different table layout. The last two, sharding and splitting the app, change how the whole system and the whole team work. You put those off as long as you honestly can.

The order to try things

Archive
→
Partition
→
Search index
Shard
→
Split the app
The growth steps in this module, cheapest first. Most apps never need to shard, and many never need to split the app either.

A useful habit: for every step, write down the trigger that tells you it's time. "We'll shard when the primary's write load passes 70% of what we measured it can do" is a plan. "We'll shard because we're getting big" is a guess.

Remember also what you already have. Scaling up (Module 4) still works: a database server with 1 TB of RAM and 64 cores is a normal thing to rent. Read replicas (Module 5) and caching (Module 3) still take most reads away. Many companies much bigger than Tiffin run their main database on one primary for years. The goal is not to get to sharding. The goal is to avoid needing it.

Module 14 covers partitioning data across many machines in full depth: range and hash partitioning, consistent hashing, hot spots and rebalancing. This module stays practical: what to do, in what order, and why.

Find Tiffin's fastest-growing data

Tiffin adds a feature: the customer app logs every screen view (about 40 per session, 750,000 sessions a day) into a screen_views table in the main database, at ~150 bytes per row. How much does it add per year, and what would you do about it?

Show one answer

40 x 750,000 = 30 million rows a day. At 150 bytes that's 4.5 GB a day, or about 1.6 TB a year, before indexes. That's more than all the order tables put together, for data that no customer-facing screen ever reads.

It doesn't belong in the main database at all. Send these events to a log pipeline or an analytics store, and keep the transactional database for transactions. The cheapest data to manage is data that never lands in the hot database.

What to remember

  1. Traffic comes and goes. Data only grows. After a few years, size becomes its own problem.
  2. Estimate size per table: rows x bytes per row, then add 50-100% for indexes. The fastest-growing table is often one you rarely think about.
  3. Big data hurts through the working set, deeper indexes and slow maintenance: backups, restores, new replicas, schema changes and deletes.
  4. Most data is cold. Typically a few percent of rows serve almost all reads.
  5. Name the actual problem before fixing it. Archive, partition, search index, shard and split the app solve different problems.
  6. Try the cheap steps first, write down a trigger for each step, and put off sharding as long as you honestly can.