sehaj/learn
System design/05 Partitioning and IDs

05.01 · concept · 12 min

Sharding: range, hash, directory

Splitting one dataset across several databases: choosing a shard key, the three ways to map keys to shards, and what you give up once the data is split.

The problem: one primary isn't enough

At a hundred times today's traffic (01.05 Back-of-the-envelope estimation), Snip stores a billion new links a month: 30 TB over five years, and growing by 6 TB a year. Reads are mostly served by the cache, but every new link, every cache miss and every dashboard query lands on one primary. Its disk, its memory for hot indexes, its vacuum (04.05 MVCC) and its backups (a 30 TB restore takes many hours) all get harder every month. 04.08 Scaling a relational database's cheaper steps have been used up.

Sharding splits the data itself: each of several databases (shards) holds part of the rows, and each row lives on exactly one shard. Every shard is an ordinary Postgres primary with its own replicas, a fraction of the size.

The shard key decides everything

Each row is placed by its shard key. The rule for choosing it: the queries you run most should need only one shard. Snip's hot query is the redirect, "find the link for code x7Kp", so the natural key is code.

That choice has a cost. The dashboard query, "Ana's links, newest first", now doesn't know which shards hold Ana's links: they're spread across all of them. It must ask every shard and merge the answers (scatter-gather), which is as slow as the slowest shard (03.12 Tail latency and why p99 matters). The usual fix is a second, denormalised copy keyed the other way (04.12 Denormalisation): a small links_by_owner table, sharded by owner_id, holding each owner's codes.

If Snip shards byRedirect by codeOwner's dashboardWatch out for
codeOne shardEvery shard, unless there's a copy by ownerKeeping the owner copy in step
owner_idMust find the owner first (put the shard in the code?)One shardOne huge customer filling a shard

Good shard keys have many distinct values, spread traffic evenly, and appear in nearly every query. Tenant IDs (workspace, organisation) are popular because most queries stay inside one tenant, until one tenant grows bigger than a shard.

Three ways to map keys to shards

Range. Each shard holds a contiguous range of keys: codes 0–F on shard 1, G–P on shard 2, and so on. Range queries ("every key between these two") stay on one shard, and splitting a full range in two is natural. The danger is keys that arrive in order: if the key is a timestamp or an increasing ID, every new row goes to the last range, and one shard takes all the writes.

Hash. Hash the key and use the hash to pick the shard. Neighbouring keys land far apart, so load spreads evenly, including for increasing IDs. The price: range queries on the key now touch every shard, and the obvious mapping, hash(key) mod N, moves almost every key when N changes. That's the problem 05.03 Consistent hashing solves.

Directory. A lookup table records which shard holds each key, or each group of keys (a tenant, a range). It's the most flexible: move one big customer to a shard of their own by changing one row. The cost is an extra lookup on every request, and the directory itself must be fast, cached and highly available, since nothing works without it.

Logical shards on physical machines

Instead of one shard per machine, most systems create many more logical shards than machines, and place several on each: say 256 logical shards on 8 machines, 32 each. The key maps to a logical shard, which never changes; a small table maps logical shards to machines, which does. Growing to 16 machines then means moving whole logical shards, not re-splitting rows (05.04 Rebalancing without downtime).

Snip, sharded by code
x7KpSnip appShard routercode → shardPostgres 1shards 0–31Postgres 2shards 32–63Postgres 8shards 224–255

3 steps. Use Next and Back to follow the request.

Text version
  1. A redirect for x7Kp arrives. The app hands the query to the shard router, a library in the app or a proxy in front of the databases.
  2. The router hashes x7Kp to logical shard 41, looks up which machine holds shard 41 (Postgres 2), and sends the query there. One shard, one query.
  3. Each machine holds 32 of the 256 logical shards, about 4 TB, with its own replicas and backups. To grow, whole logical shards move to new machines.
  • Snip app → Shard router
  • Shard router → Postgres 1
  • Shard router → Postgres 2: x7Kp
  • Shard router → Postgres 8

The router can live in three places: a library in the application (no extra hop, but every service needs it), a proxy between the application and the databases, or the database itself, as in the distributed databases of 04.10 NoSQL families.

What you give up

Once rows live on different machines, features that one database gave you for free stop working across shards:

  • Transactions across shards. Updating rows on two shards atomically needs a distributed protocol (08.11 Two-phase commit vs sagas); most designs avoid it by keeping related rows on one shard.
  • Joins across shards, done in application code or avoided with copies.
  • Unique constraints across shards. A unique email on users sharded by user_id can't be enforced by any one shard. (Snip's codes are fine: the code is the shard key, so its shard checks it.)
  • Auto-increment IDs, since each shard counts separately (05.05 Distributed ID generation).
  • Simple operations. Schema changes, backups and failovers now happen N times.

That's why sharding comes last: shard when the data or write load truly outgrows one well-tuned primary, and choose the key by studying the queries first.

Seen in production

Notion

2021

Postgres, sharded by workspace

Notion's single Postgres was nearing its limits: CPU spikes, and vacuum stalling badly enough to risk transaction ID wraparound. They sharded every table reachable from block by workspace ID, since each block belongs to one workspace and users mostly work within one. They chose 480 logical shards, 15 per machine on 32 machines, because 480 divides evenly into many machine counts. The move used double writes, a three-day backfill and dark reads comparing both databases, then a few minutes of scheduled downtime.

What we found480 logical shards on 32 machines, keyed by workspace ID so most queries stay on one shard; their first lesson was to shard earlier.

Herding elephants: lessons learned from sharding Postgres at Notionnotion.com ↗

Slack

2020

MySQL, moving to Vitess

Slack's MySQL clusters were partitioned by workspace ID. The largest customers' shards reached the biggest machines available, big teams created hot spots that forced most shards to be over-provisioned, and a shard outage took down every customer on it. Slack moved to Vitess, a sharding layer for MySQL, which let tables use other keys: messages are now sharded by channel ID. By late 2020 Vitess served 99% of Slack's query load, 2.3 million queries a second at peak.

What we foundSharding by workspace kept each team on one shard, until the largest teams outgrew the largest hardware.

Scaling Datastores at Slack with Vitessslack.engineering ↗

Try it

Design it yourself firstmidsharding

Your Pastebin (from 04.01 Relational modeling and normalisation) now stores 50 TB of paste metadata and needs sharding. The top queries are "read paste by ID" (99% of traffic) and "list my pastes, newest first". Choose the shard key and mapping, and say how each query is answered.

10:00
Predict first

Snip shards links by hash of code. Which query gets slower?

Commit to an answer before you read on.

Recap

Interview questions

MidfreqMust-knowDesign

How would you shard the database for a URL shortener?

MidfreqMust-knowConcept

Compare range-based and hash-based sharding.

Report a mistakeEdit this page