Partitioning Strategies: Range vs Hash vs Directory
Range wins range scans but hotspots on sequential keys, hash spreads evenly but loses ranges, directory adds a flexible routing hop; map every dominant query to its partitions.
Splitting past one machine
When one machine can no longer hold the dataset or absorb the write rate, the only real fix is horizontal partitioning (sharding): split the rows across many nodes so each node owns a slice. Contrast this with vertical partitioning (splitting a wide table into narrow ones by column) and functional partitioning (giving each service its own database). Vertical and functional buy some headroom, but only horizontal partitioning scales writes and dataset size without bound, so it is the technique interviewers mean by "shard it."
The design choice is the partition function: given a key, which partition owns it. Three families dominate.
Range partitioning assigns contiguous key ranges to partitions (users A to F on p0, G to M on p1; or time ranges for events). Its superpower is range scans: "all orders from last Tuesday" touches one or two partitions. Its curse is hotspots on sequential keys. If you range-partition by an auto-increment ID or a timestamp, every new write lands on the highest partition, so one node absorbs 100% of the write traffic while the rest sit idle. This is the single most common partitioning mistake.
Hash partitioning applies a hash to the key and assigns by the result (often
hash(key) mod N). It spreads load evenly and kills sequential hotspots, because adjacent keys
scatter. The cost is that you lose efficient range queries: "orders from last Tuesday" now fans
out to every partition (scatter-gather). The other trap is hash mod N specifically: change N (add
a node) and almost every key remaps, forcing a near-total reshuffle. Consistent hashing exists to fix
exactly that.
Directory (lookup-based) partitioning keeps an explicit routing table mapping key ranges or key groups to partitions, maintained in a coordination service (ZooKeeper/etcd) or a metadata store. It gives maximum flexibility: split a hot range, move a heavy tenant to its own node, rebalance surgically. The price is an extra lookup hop on the request path and a routing service you must keep highly available, since it is now on the critical path.
Secondary indexes get partitioned too
A local (document-partitioned) index stores each partition's index alongside its own data, so a query on a non-partition-key column must scatter-gather across all partitions and merge (DynamoDB LSI, Elasticsearch by default). A global (term-partitioned) index partitions the index itself by the indexed term, so a lookup hits one index partition, but writes must update an index partition that may live on a different node, making writes slower and asynchronous (DynamoDB GSI). The index does not live for free on one node.
Interview nuance: always map the dominant queries onto the partition scheme out loud. "This query hits one partition, that one is scatter-gather bounded by the slowest node." Interviewers are checking whether you know which reads got expensive, not just that you sprinkled the word "shard."
| Strategy | How keys map | Wins | Costs |
|---|---|---|---|
| Range | Contiguous key ranges (A-F, G-M, N-Z) or time ranges | Range scans hit 1 or 2 partitions | Sequential keys hotspot the highest partition |
| Hash | hash(key) mod N scatters adjacent keys | Even spread, no sequential hotspot | Range scans fan out to every partition; changing N remaps almost every key |
| Directory | Explicit lookup table maps keys to partitions | Surgical rebalance: split a hot range, move a heavy tenant | Extra lookup hop plus a routing service that must stay highly available |
Recap: horizontal partitioning is the only way to scale writes and data past one node; range wins range scans but hotspots on sequential keys, hash spreads evenly but loses ranges and reshuffles on mod N, directory adds a flexible routing hop, and secondary indexes are either scatter-gather locals or write-costly globals.
Match each behavior to the partitioning strategy that produces it.
Apply
Your turn
The task this lesson builds to.
Design the partitioning scheme for a 20 TB messaging store doing 200k writes/sec; pick a partition strategy and defend it against skew.
Think about
- What does range vs hash vs directory partitioning optimize and cost?
- How do local vs global secondary indexes work across partitions?
- How does each query map to partitions?
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Design the partitioning scheme for Stripe-style payment events at 50 TB and 300k events/sec, where the two dominant access patterns fight each other: (1) low-latency single-object reads by event_id, and (2) an analytics/reconciliation job that must scan 'all events for merchant M in a date range.' Pick a scheme and defend it against both skew and the range-scan requirement.
Think about
- How can one scheme serve both hash-friendly point reads and range-friendly merchant scans?
- What does a whale merchant at 40% of volume do to its partition, and what splits it?
- How does encoding routing information into the event_id avoid a fan-out?