System designCore13 min

Sharding & partitioning

When one database can't hold all the data or take all the writes, you split the rows across many. Sharding buys almost unlimited room, and the shard key you pick decides which queries stay fast and which get slow for good.

1 · The problem

One database can only grow so far

An online store keeps every order in one database. It has 4 TB of orders and grows by a terabyte a year. At the holiday peak it must take 30,000 new orders a second, and load tests show the server tops out around 10,000. Something has to give.

Try the cheap fixes first, because each one keeps the database simple:

Fix

What it helps

Where it stops

A bigger server

Everything, with no code changes

The biggest machine you can buy, and its price

Read replicas

Reads: copies of the data answer queries

Every write still goes to one leader

A cache

Repeated reads of the same data

Writes, and data read only once

Archiving old rows

Size: orders from 2019 move to cheap storage

Write rate

None of these raises the write limit. For that the data itself has to be split, so that different servers own different rows. Partitioning means splitting a dataset into pieces. Sharding is partitioning by rows across separate servers: each shard has the same tables but its own subset of the rows, and each shard is a complete database of its own.

With four shards, each takes a quarter of the peak: 30,000 ÷ 4 = 7,500 writes a second, below the 10,000 one server can handle. Each shard is still replicated, so losing a server affects one shard until a replica takes over, never the whole store:

Order serviceRouterknows which shard is whichShard 1leader + 2 replicasShard 2leader + 2 replicasShard 3leader + 2 replicasShard 4leader + 2 replicas7,500/s7,500/s7,500/s7,500/sOrder serviceRouterknows which shard is whichShard 1leader + 2 replicasShard 2leader + 2 replicasShard 3leader + 2 replicasShard 4leader + 2 replicas7,500/s7,500/s7,500/s7,500/s

Sharding and replication solve different problems and are used together: shards split the writes, replicas keep each shard alive and serve its reads.

Sharding is one of three ways to split data, and the other two are simpler, so check them first:

  • Vertical partitioning splits a table's columns: the order's large, rarely read gift message moves to its own table.
  • Functional partitioning gives each feature its own database: orders, the product catalog and reviews no longer share a server.
  • Horizontal partitioning, or sharding, splits the rows. It is the only one that keeps scaling, and the only one that changes how every query works.

The names vary by product. MongoDB and Elasticsearch say shard; DynamoDB and Kafka say partition; HBase says region. The idea is the same. One more word: databases can also partition a table inside one server, which helps manage big tables but adds no capacity.

2 · The big decision

The shard key decides which queries stay fast

Every row needs a rule that says which shard owns it. The column the rule uses is the shard key (or partition key). A query that names the shard key goes to one shard. A query that doesn't has to ask all of them. So the right key depends on the questions you ask most.

The store asks three questions: show a customer's orders (on every visit to the account page, by far the most common), open order 7731 (from an email link), and what sold today (once a night, for a report). Here is how three candidate keys fare:

Shard key

Writes

"My orders"

Verdict

Order date, in ranges

Every new order lands on the shard holding today: one hot shard

Every shard, since a customer's orders span years

Fails at the peak

Hash of the order ID

Spread evenly

Every shard: orders are scattered

Even, but the main query got slow

Hash of the customer ID

Spread evenly across millions of customers

One shard: all of a customer's orders live together

The fit, if order IDs carry the customer

The last row needs one trick. Open order 7731 names only the order, so the router can't tell which shard to ask. Fix it by building the customer's shard into the order ID itself (for example c51-7731), or by keeping a small lookup table from order ID to customer. The nightly report reads every shard either way; it runs once a night, so that is fine.

A good shard key has five properties:

  • Many distinct values. Shard by country and you can never have more shards than countries; shard by continent and a cluster has at most seven useful shards.
  • Evenly used values. If most rows share a few values, those shards carry most of the load (Hot partitions & skew covers the famous customer or celebrity).
  • Not always increasing. Timestamps and auto-increment IDs send every insert to the shard holding the newest values.
  • Never changes. A row whose key changes has to move to another shard.
  • In your main queries. So most requests touch exactly one shard.

Take the time to choose. Changing the shard key later means moving every row to a new layout while the system is live, which is one of the riskiest jobs in data engineering.

3 · Placing rows

Ranges, hashes or a directory: three ways to place a row

Once you have the key, you need the rule that turns a key value into a shard. There are three common rules, plus one for geography:

Rule

How it places a row

Good at

Watch out for

Range

Each shard owns a span of keys: A–F, G–M, and so on

Range queries: neighbors sit together, in order

Hot spots when new keys cluster at one end

Hash

A hash of the key picks the shard

Spreading load evenly with no map to maintain

Range queries ask every shard; growing needs care

Directory (lookup)

A lookup table maps each key, or group of keys, to a shard

Full control: move one big customer on their own

The directory must be fast, cached and never down

Geographic

The user's region picks the shard

Data residency laws, and low latency for local users

One busy region overloads its shards

Real systems often combine two. A compound key has a partition part and a sort part. Hash the customer ID to pick the shard, then keep that customer's orders sorted by date inside it. My orders, newest first becomes one shard and one sorted read. Cassandra's partition and clustering keys, and DynamoDB's partition and sort keys, work exactly like this.

Lookup tables become more flexible with virtual shards: the key maps to one of many small logical shards, say 1,024, and a small table maps those onto the physical servers. The application only ever sees the logical shard, so moving one to a new server is a change to the small table, not to the code.

4 · Reading it back

A query without the shard key asks every shard

Something has to know which shard owns what. Three places are common:

  • A routing tier, such as MongoDB's mongos: applications talk only to it, and it holds a cached copy of the shard map.
  • The client library, which keeps the map itself and connects straight to the right shard.
  • Any node, as in Cassandra: the client asks any server, which forwards the request to the owner.

Whichever you pick, the map itself lives somewhere highly available, and every router caches it.

A query with the shard key is targeted: one shard answers. A query without it is a scatter-gather query. The router sends it to every shard, waits for all of them, and merges the results:

Refunds pending?Routermerges 4 answersShard 1Shard 2Shard 3Shard 4slow todayRefunds pending?Routermerges 4 answersShard 1Shard 2Shard 3Shard 4slow today

"Which orders have a refund pending?" doesn't name a customer, so all four shards answer, and the reply waits for the slowest of them.

Scatter-gather costs more than its share of work. Every shard spends effort on every such query, so adding shards doesn't add capacity for it. And the answer waits for the slowest shard. If each of 16 shards is slow 1% of the time, a query that asks all of them is slow 1 − 0.99¹⁶ ≈ 15% of the time.

Sorting and limits are merged at the router. For the 10 newest pending refunds, each shard returns its own 10 newest, and the router merges 40 rows and keeps 10. Paging deep into such results gets expensive, because to skip 1,000 rows the router has to fetch 1,010 from every shard.

Secondary indexes face the same choice. An index on status can live with the rows or be split by the indexed value:

Aspect

Local index

Global index

Where it lives

Each shard indexes only its own rows

Its own partitions, split by the indexed value

A write

Updates the row and the index on one shard, together

Also updates the index's partition, usually a moment later

A query on status

Asks every shard

Asks one index partition

Fresh?

Always

Can lag briefly: DynamoDB's global indexes are eventually consistent

5 · The catch

Anything that spans shards gets hard

Inside one database, a join or a transaction across tables is routine. Across shards, each becomes a distributed problem. Sharding works when you design the data so they are rare:

Need

Why it hurts across shards

What to do instead

Joins

The rows live on different servers, so the database can't join them for you

Shard related tables by the same key: a customer, their orders and their addresses together

Small shared tables

Every order needs tax rates and currencies

Copy reference data to every shard, and accept a short delay when it changes

Transactions

An atomic change across shards needs a protocol like two-phase commit: slower, with new failure modes

Keep each transaction on one shard; use sagas when you can't (Distributed transactions: 2PC & sagas)

Unique IDs

Each shard's auto-increment counter starts at 1, so IDs collide

Generate IDs that are unique everywhere (Unique ID generation)

Uniqueness rules

"One account per email" must check every shard

A separate table keyed by email, or a check through a global index

Operations multiply too. Every schema change runs on every shard. Backups are per shard, and restoring one shard to last Tuesday leaves it out of step with the others. Monitoring needs a view across all of them. None of it is impossible, and all of it is permanent: sharding is a decision you live with for the life of the system.

That is why it comes last. If the data and the writes fit on one well-sized server for the next few years, with replicas for reads and a cache in front, that is the better design. Shard when the write rate or the data size says you must, or when the law says some data must live in one country.

6 · Growing

Add shards without moving everything

The store grows, and four shards become five. How much data has to move? It depends on the rule, and the obvious rule is the worst one.

The obvious rule is hash mod N: shard = hash(key) mod 4. Change N to 5 and a key stays put only if its hash gives the same remainder for 4 and for 5, which is true for just 1 key in 5. So 80% of all orders move, at the busiest time, just to add 20% more capacity. Consistent hashing shows the problem in full.

Hash mod N
80 %
1,024 fixed partitions
20 %
Consistent hashing
20 %
Ideal: the new server's share
20 %

Rows that move when 4 shards become 5. Mod N: a key stays only if hash mod 4 = hash mod 5, true for 1 key in 5. The other rules move about 1 ÷ 5 of the rows, the share the new server should end up holding anyway.

Many fixed partitions is a common answer. Create far more partitions than servers on day one, 1,024 for 4 servers, 256 each, and never change that number. The key-to-partition rule stays fixed forever; growing only moves whole partitions. The fifth server takes about 51 from each of the others. The catch is that the number can't easily change later: Elasticsearch, for example, sets an index's number of primary shards when the index is created.

Splitting ranges is the other answer, used by range-sharded systems like MongoDB and HBase. A range that grows too big splits in two, and a balancer moves halves to emptier servers. It adapts to the data, but it can't help when every new write targets the same end of the range.

Moving a partition safely follows the same steps whatever the rule:

  1. Copy

    Copy the partition's rows to the new server while the old one keeps serving.

  2. Catch up

    Replay the writes that arrived during the copy, until the new copy is only moments behind.

  3. Switch

    Once the copy has caught up, update the shard map: reads move to the new server first, then writes.

  4. Refresh and clean up

    Routers drop their cached map and learn the new owner; the old copy is deleted later, once nothing reads it.

Check yourself

5 questions

1. The orders database takes 30,000 writes/s at peak, and one server handles about 10,000. Which change raises the write limit?
2. Orders are sharded by order date, in ranges. What happens on the busiest sale day of the year?
3. Orders are sharded by a hash of the order ID. Which shards does "show customer 51's orders" ask?
4. A query asks all 16 shards, and each shard is slow 1% of the time, independently. How often is the query slow?
5. Keys map to 1,024 fixed partitions spread over 4 servers. You add a fifth server. Roughly how much data moves?

Takeaways

Remember this

  • Shard when writes or data outgrow one server; replicas, caches and bigger machines only postpone that for reads and size.
  • The shard key decides everything: many distinct values, evenly used, not always increasing, never changing, and present in your main queries.
  • Queries with the shard key touch one shard; queries without it scatter to all and wait for the slowest.
  • Keep data that is used together on the same shard, because joins, transactions and unique checks across shards are expensive.
  • Never place rows with hash mod N; use many fixed partitions, splitting ranges or consistent hashing so growing moves only a fraction.