Sharding a Database: Partition Strategies That Survive Growth
Sharding is what you do when one machine can no longer hold your data or your write load — you split the dataset across nodes, each owning a slice. It is also the single most expensive architectural decision most teams ever make, because the shard key you pick quietly becomes a constraint on every query, every transaction, and every product feature for the next five years.
So before anything else: exhaust the alternatives. Bigger box (vertical scaling gets you surprisingly far in 2026), read replicas for read load, caching for hot data, archiving cold rows, and fixing your indexes. Sharding is for when writes or raw data volume outgrow one primary — not for slow queries.
The two fundamental strategies
Range partitioning assigns each shard a contiguous key range: users A–F on shard 1, G–M on shard 2. HBase and CockroachDB (ranges), and time-partitioned tables everywhere, work this way.
- Range scans are cheap — "all orders from June" touches one or two shards.
- Sorted access patterns are natural.
- The failure mode is skew: if the key is time-ordered, all new writes land on the last range, and one shard does all the work while the others sit idle. Monotonic keys plus range partitioning is the classic self-inflicted hot spot.
Hash partitioning runs the key through a hash and assigns by hash value, scattering adjacent keys across shards uniformly.
- Load spreads evenly by construction.
- Range queries die — "orders from June" now touches every shard (scatter-gather).
- Resharding with naive
hash(key) % Nremaps nearly everything when N changes, which is why real systems use consistent hashing or fixed virtual partitions (Cassandra's vnodes, the pre-created slot approach) so ownership moves in small chunks.
Most large systems end up hybrid: hash on a tenant or user ID to pick the shard, range-organize within the shard so that per-user time queries stay local.
Choosing the shard key
The shard key decides which queries stay on one node and which fan out to all of them. The selection criteria, in priority order:
- It appears in your hottest queries. If 95% of queries are "by user," shard by user ID. A query that cannot supply the shard key must hit every shard.
- It distributes load, not just data. Even row counts per shard are worthless if one key gets all the traffic.
- It keeps transactional units together. Rows you update atomically together should share a shard, because cross-shard transactions demand sagas or two-phase commit.
- High cardinality. Sharding by country gives you a US shard that dwarfs the rest; you cannot split a single key value.
For B2B SaaS, tenant ID is usually right and doubles as an isolation boundary. For consumer products, user ID. For time-series, device or metric ID hashed, with time ranges inside.
The hot partition problem
Even a good key develops celebrities. One tenant becomes 30% of traffic; one device firehoses telemetry; a viral post concentrates writes on one row's shard. Standard responses, escalating in effort:
1. Cache in front — absorb read-heavy hot keys before the shard sees them
2. Salt the key — user123 becomes user123#0..user123#7, spreading one
key over 8 partitions; reads must query all salts
3. Isolate the whale — move the hot tenant to a dedicated shard
(directory-based placement makes this possible)
4. Split the range — systems like CockroachDB/Spanner detect hot ranges
and split/rebalance automatically
The monitoring requirement is per-shard, per-key-prefix traffic metrics. Shard-averaged dashboards hide exactly the thing that will page you.
What sharding actually costs
The brochure says "linear scalability." The invoice lists:
Cross-shard queries. Any query missing the shard key becomes scatter-gather: send to all N shards, merge results, pay the latency of the slowest shard. Top-K and pagination across shards are genuinely awkward — every shard must return its own top K before you can merge.
Cross-shard joins. Mostly you stop joining and start denormalizing, or you maintain global lookup tables (small, replicated to every shard) for reference data.
Cross-shard transactions. ACID within a shard, distributed-systems problems across shards. Design entity boundaries so that money movements, inventory decrements, and other invariant-carrying updates are single-shard.
Unique constraints and auto-increment IDs. No single node can enforce global uniqueness anymore — you need distributed ID generation instead of AUTO_INCREMENT.
Operational surface. Schema migrations run N times. Backups, failovers, and capacity planning multiply. A routing layer (application-side, or a proxy like Vitess/Citus) becomes tier-zero infrastructure.
Resharding without a maintenance window
You will get the shard count wrong; plan the correction upfront. The standard live-migration playbook:
- Over-partition logically from day one — e.g., 4,096 virtual partitions mapped onto 8 physical nodes. Resharding becomes "move some virtual partitions," not "rehash the world."
- Dual-write to old and new placement while backfilling historical data.
- Verify with checksums; route reads to the new placement behind a flag.
- Cut over writes, keep the old copy until confidence is high.
Directory-based placement (an explicit partition-to-node mapping table) is what makes moves and whale isolation routine instead of terrifying. Every serious sharded system converges on this.
Takeaways
Shard only when a single primary genuinely cannot carry the write or storage load. Pick the shard key from your query patterns, not your ER diagram — it is the most permanent decision in the system. Hash for uniformity, range for scans, hybrid for both. Over-partition logically on day one so resharding is a rebalance, not a rewrite. And treat every cross-shard operation as a design smell to eliminate, because the shards you never have to coordinate are the ones that scale.
Keep reading
Database Replication: Leader-Follower, Quorums, and the Lag Nobody Budgets For
Sync vs async replication, why failover loses writes, and how quorum systems skip the leader entirely. The trade-offs that set your durability story.
Designing a Distributed Job Scheduler That Doesn't Fire Twice (Usually)
Delayed jobs, cron at scale, and work claiming with SKIP LOCKED. Why every scheduler promises at-least-once and what that forces on your handlers.
Designing a News Feed: Fan-Out on Write vs Fan-Out on Read
Precompute every follower's timeline or assemble it per request? The celebrity edge case that breaks the pretty answer, and the hybrid everyone ships.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
- No comments yet. Be the first.