Concepts

Placing data

Data

Sharding & Partitioning

Splitting one dataset across many machines, and what the split costs you.

Range partitioning keeps ordering and invites hot spots; hashing kills both.

rangehashhot spotsscans

Try it

Move the dials — the sentence under the picture changes.
Partitioning
600 keys · 6 shards
Key space (low ids on the left)contiguous ranges — neighbouring keys share a shardKeys per shardshard 098 keys · 1.0×shard 198 keys · 1.0×shard 285 keys · 0.8×shard 399 keys · 1.0×shard 4107 keys · 1.1×shard 5113 keys · 1.1×fair share
With evenly spread ids, range partitioning distributes fine and keeps keys ordered, so a scan for “everything between X and Y” touches one shard. Raise the key skew to see what a monotonic id does to it.

In plain words

When one database machine cannot hold all the data, or cannot serve all the reads and writes, you split the data across several machines. Each piece is a shard. The rule that decides which row lives on which shard is the whole design: it decides how evenly the load lands and which queries stay cheap.

every insertAppRouterwhich shard?Shard Aids 1–50MShard B50–100MShard C100–150MShard D150M+ ← all new orders
Range sharding on an always-increasing key: three shards hold archives, one does all the work.

Three ways to split

By range
keeps order

Contiguous ranges of the key: A–F here, G–M there. A range query — "orders from March to April" — touches one or two shards. A shard that grows too big is split in place.

The trap: a key that only goes up (timestamp, auto-increment id) sends every new write to the last shard.

By hash
spreads load

shard = hash(key) % N (or a ring — see consistent hashing). Writes spread evenly whatever the keys look like.

The trap: order is gone. A range query has to ask every shard and merge — a scatter-gather that is as slow as the slowest shard.

By directory
flexible

A lookup table: key → shard. You can move one noisy customer to their own shard by editing a row.

The trap: a lookup on every request, and the table is now the most critical thing you run.

Turn the skew slider up in the widget to watch range sharding pile onto one shard, then switch to hash and watch it flatten — and watch the range query get expensive at the same moment.

Choosing the shard key

This is the decision you cannot cheaply undo. A good key does three things that pull against each other:

  1. Spreads writes

    Many distinct values, none of them dominant. customer_id yes; country no (one shard for the US, one for everyone else).

  2. Keeps your main queries on one shard

    If the account page runs "all orders for customer 42", shard by customer and that query is one shard, every time.

  3. Does not create a giant

    One customer with 40% of all orders is a shard on their own whatever you do. Know who they are.

The usual escape from the range-versus-hash trade is a compound key: hash the first part for spread, keep order inside it for locality.

Hash the customer, keep their orders in time orderSQL
-- Shard chosen by hash(customer_id); inside a shard, rows are clustered by time.
-- "All of customer 42's orders in March" = one shard, one range scan.
CREATE TABLE orders (
  customer_id  bigint      NOT NULL,   -- shard key: many values, spreads writes
  created_at   timestamptz NOT NULL,   -- sort key: keeps a customer's rows together
  order_id     bigint      NOT NULL,
  total        numeric(12, 2),
  PRIMARY KEY (customer_id, created_at, order_id)
) PARTITION BY HASH (customer_id);
Routing in the applicationTypeScript
function shardFor(customerId: bigint): Shard {
  const n = Number(customerId % BigInt(SHARDS.length));  // or a consistent-hash ring
  return SHARDS[n];
}

// One shard: cheap.
const orders = await shardFor().query(
  "SELECT * FROM orders WHERE customer_id = $1 AND created_at >= $2", [, "2026-03-01"]
);

// Every shard: scatter, then gather and sort. Avoid on the hot path.
const recent = (await Promise.all(SHARDS.map((s) => s.query("SELECT * FROM orders ORDER BY created_at DESC LIMIT 20"))))
  .flat().sort(byCreatedAtDesc).slice(0, 20);

Where it goes wrong

Cross-shard operations

"Transfer $50 from customer 42 to customer 77" — two customers, two shards, and no single transaction spans them. A crash between the two writes loses or doubles money.

Fix

Keep things that must change together on the same shard (shard by account, not by customer, if transfers are between accounts), or use an outbox and make each step idempotent so it can be replayed.

  • Resharding. Going from 4 shards to 8 means moving data while still serving traffic. Plan for it on day one: start with more logical shards than machines (say 64), several per server, so growth is moving whole shards rather than splitting rows.
  • Hot tenants. An even distribution of keys is not an even distribution of activity. Watch per-shard load, not just per-shard size.
  • Joins. A join across shards is a scatter-gather on both sides. Denormalise (copy the columns you need) or keep the tables you join on the same shard key.
  • The one query nobody thought of. Sharding by customer makes "top 10 products today" a query across every shard. Analytics gets its own copy of the data.

Take this with you

  • The one idea: the shard key decides both how evenly load lands and which queries stay cheap, and it is very hard to change later.
  • In an interview, contrast range vs hash, show the monotonic-key trap, and propose a compound key that fits the main query.
  • At work, look at your biggest customer. If their data is one shard, that shard is your ceiling.