“We’re resharding next month”
During his internship Kabir hears the phrase “we’re resharding the orders database next month” in a meeting. Later, Riya asks him what it means. He isn’t sure either, so they both go to Arjun Sir.
Sir, the orders table has 40 crore rows and the database server is almost full. The team says they’ll “shard” it. Is that like making a backup copy?
No, a copy is replication, our next lesson. Sharding means splitting the data: some users' orders on machine 0, others on machine 1, and so on. Each machine holds only a part, so together they hold far more and share the work.
Like a library that puts books A to F on one shelf and G to M on another?
Exactly, Riya. That’s range partitioning. Another way is to hash each key, like a lucky-number formula, so the data spreads evenly. The big decision is the partition key: the field that decides where each row goes.
And if we choose the wrong key?
Then one shard gets overloaded while others sit idle, or simple queries suddenly have to ask every machine. Today you’ll see what happens when a shard is added, why one celebrity kitchen can melt a shard, and why partitioning by date makes “today” a hotspot.
What are partitioning and sharding?
Partitioning divides a large dataset into smaller, independent subsets called partitions, so that storage and processing can be spread across resources. Horizontal partitioning splits rows by the value of a partition key; when the partitions live on different servers it is called sharding. Common strategies are range partitioning (contiguous key ranges), hash partitioning (a hash function of the key) and directory/list partitioning (an explicit mapping). Vertical partitioning splits columns into separate tables.
Partitioning means cutting a huge table into smaller pieces by some rule, for example by user id or by date. Sharding is when those pieces are put on different machines, so each machine stores and handles only part of the data.
Words you’ll meet in this lesson
Partitioning across the big data stack
| HDFS | files split into 128 MB blocks spread over DataNodes |
|---|---|
| Spark | DataFrames split into partitions processed in parallel; shuffles repartition by key |
| Kafka | topics split into partitions; order is kept only within a partition; partition count limits consumer parallelism |
| Cassandra / DynamoDB | partition key hashed onto a ring of nodes |
| Data lake / warehouse | tables partitioned by date or region folders; queries skip partitions they don’t need |
| Sharded SQL | MySQL/PostgreSQL shards behind a router (for example Vitess or Citus) |
Partitioning at three levels
Beginner Shelves in a library, coaches in a train
- Partitioning means splitting a big collection into smaller parts. A library splits books across shelves; a train splits passengers across coaches.
- Range: shelf 1 for A–F, shelf 2 for G–M. Easy to find a range (“all books starting with C”), but some shelves can get crowded.
- Hash: a formula turns each name into a number and picks a shelf. Books spread evenly, but “all books starting with C” means checking every shelf.
- Directory: a notebook at the entrance lists which shelf each book is on. Flexible, but the notebook must always be available.
The key question: which field decides the shelf? Choose the one most queries use, with many different values.
Intermediate Strategies, keys and where partitioning shows up
- Horizontal vs vertical: horizontal partitioning splits rows (sharding when the parts are on different machines); vertical partitioning splits columns into separate tables or stores.
- Good partition key: high cardinality (many distinct values), even distribution, and present in most queries. User id is often good for an app; city is usually poor (few values, very uneven).
- Routing: a router or the client library computes the shard from the key. Queries with the key go to one shard; queries without it must ask every shard and merge results (scatter-gather).
- Everywhere in big data: HDFS blocks, Spark partitions, Kafka topic partitions (ordering is guaranteed only within one partition), Cassandra and DynamoDB partition keys, and date folders in a lake (
date=2026-10-19/) used for pruning.
Advanced Rebalancing, consistent hashing and hot keys
- The mod-N problem: with
hash(key) % N, changing N from 4 to 5 moves about 80% of keys, a huge data migration. - Consistent hashing: place shards and keys on a ring; each key belongs to the next shard clockwise. Adding a shard moves only about 1/N of keys. Virtual nodes (many points per shard) keep the load even. Used in Cassandra, DynamoDB and many caches.
- Fixed partitions: another approach (Kafka, Elasticsearch, Couchbase) creates many more partitions than machines up front and moves whole partitions when nodes join.
- Hot keys: one huge customer or one viral event overloads one shard. Fixes: split the key with a suffix (key + random 0–9), cache it, or give it a dedicated shard.
- Secondary indexes: a local index per shard makes writes cheap but reads scatter-gather; a global index partitioned by the indexed field makes reads cheap but writes touch several shards.
- Cross-shard work: joins and transactions across shards are slow and complex, so data that is used together should share a partition key (for example user id on both orders and payments).
Exam tip: Define partitioning and sharding, then explain range, hash and directory partitioning with one example and one advantage and disadvantage each. Mention how to choose a partition key and the hot-spot problem. A small diagram of keys going to shards earns extra marks.
Range, hash, directory and composite partitioning
Range partitioning
Contiguous ranges of the key: dates, ids 1–10 lakh, names A–F. Great for range queries; risk of hotspots on the newest range.
Ordered rangesHash partitioning
hash(key) decides the partition. Even spread, no hotspots from ordering; range queries must hit every partition.
Even spreadDirectory / list
A lookup table maps keys (or groups like regions) to partitions. Very flexible; the directory must be fast and highly available.
Lookup tableComposite
Combine methods: hash on user id to pick a shard, then range on date inside it. Common in Cassandra-style keys.
Hash + rangeSide by side
| Range | Hash | Directory | |
|---|---|---|---|
| How a row is placed | Which range its key falls in | hash(key) % N or on a ring | Look up the key in a table |
| Range queries | Efficient (few partitions) | Must ask all partitions | Depends on the mapping |
| Balance | Can be uneven; newest range is hot | Even for good keys | Can be tuned by hand |
| Adding a node | Split a range | mod-N moves most keys; consistent hashing moves ~1/N | Update the directory |
| Example | Orders by month | Users by hash(user_id) | Tenants mapped to shards |
Choosing a partition key
| Candidate key | Distinct values | Spread | Used in most queries? | Verdict |
|---|---|---|---|---|
| city | about 50 | Very uneven (metros dominate) | Sometimes | Poor |
| order_date | 365 per year | All writes hit today | For reports, yes | Good for analytics, poor for live writes |
| user_id | crores | Even | Yes (my orders, my payments) | Good for the app |
| order_id | billions | Even | Only for single-order look-ups | Good spread, but user queries scatter |
Partition by what you query, not by what you store. If the app always asks “orders of this user”, partition by user id. If analysts always ask “orders on this date”, partition the lake table by date. The same data can be partitioned differently in different systems.
Sharding an orders table
Pick the partition key
TiffinGo’s app mostly asks “show this user’s orders” and “place an order for this user”. So the orders table is partitioned by
The key decides everything else: balance, which queries are fast, and which queries must visit every shard.user_id.Hash partitioning spreads users evenly
A stable hash (here MD5; never Python’simport hashlib def shard_for(user, n=4): return int(hashlib.md5(user.encode()).hexdigest(), 16) % n for u in ["U9", "U42", "U777", "U1001"]: print(u, "-> shard", shard_for(u))U9 -> shard 1 U42 -> shard 0 U777 -> shard 2 U1001 -> shard 2hash(), which changes between runs) turns each user id into a big number;% 4picks one of four shards. Similar ids land on different shards, so load spreads evenly.Range partitioning keeps dates together
For analytics, ranges of dates are natural: a report on October reads only P3. The catch: every new order goes into the newest range, so one partition takes all the writes.ranges = [("2026-01", "2026-03", "P0"), ("2026-04", "2026-06", "P1"), ("2026-07", "2026-09", "P2"), ("2026-10", "2026-12", "P3")] def part_for(month): return next(p for lo, hi, p in ranges if lo <= month <= hi) print("2026-10 ->", part_for("2026-10"), "| 2026-02 ->", part_for("2026-02"))2026-10 -> P3 | 2026-02 -> P0The trouble with % N when you grow
Going from 4 to 5 shards changesimport hashlib h = lambda k: int(hashlib.md5(k.encode()).hexdigest(), 16) keys = [f"U{i}" for i in range(1000)] moved = sum(1 for k in keys if h(k) % 4 != h(k) % 5) print(f"mod-N, 4 -> 5 shards: {moved / len(keys):.0%} of keys move")mod-N, 4 -> 5 shards: 79% of keys movehash % Nfor most keys, so most of the data would have to move. Consistent hashing reduces this to roughly one fifth (see scenario 1).Directory partitioning for special cases
A lookup table lets you place big customers deliberately and move them without rehashing anyone else. It is common in multi-tenant SaaS products. The directory itself must be fast and highly available.directory = {"tenant_acme": "shard-A", "tenant_bigbazaar": "shard-B", "tenant_small_1": "shard-C"} print(directory["tenant_bigbazaar"]) directory["tenant_bigbazaar"] = "shard-D" # move a big tenant to its own shard print(directory["tenant_bigbazaar"])shard-B shard-DQueries with and without the key
“Orders of U42” goes to one shard. “All orders from Pune today” has no user id, so the router asks all shards and merges the results.
Scatter-gather works, but it gets slower and costlier as shards grow. Such analytical questions are better answered from a data lake or warehouse copy partitioned by date.
Two queries: one shard or all shards
Step through the six frames. Left: Riya checking her orders and a support agent searching by city. Right: how the router finds the data.
- Riya opens “My orders”.
- Her last four orders appear.
- The whole list loaded in about 8 milliseconds.
- A support agent searches for all of today’s orders from Pune.
- The search works but takes 2.4 seconds.
- After the fix, the same search takes 0.3 seconds.
- The request reaches the router carrying Riya’s user id, U9.
- The router hashes the shard key: hash(U9) % 4 = 3. Only shard 3 holds her orders.
- Shard 3 returns four rows. The other three shards don’t do any work at all.
- The support search filters by city, which is not the shard key.
- So the router must scatter the query to all four shards and gather and merge the results: slower, and it grows with every shard added.
- The fix is to answer analytical questions from a copy partitioned for them: a lake or warehouse table partitioned by date and city. (Timings are illustrative.)
Real-time scenarios: partitions under pressure
Three live scenarios: adding a shard with and without consistent hashing (real numbers), a viral offer that overloads one shard, and an advanced look at the date-partitioning trade-off.
Real-time scenario 1Adding a fifth shard: who has to move?
These numbers come from the program in “Run it on your PC”: 3,000 users on 4 shards, then a 5th shard is added. Compare simple hash % N with consistent hashing.
- 3,000 users are spread over 4 shards. Traffic grows, so a fifth shard is added.
- With
hash(user) % N, changing N from 4 to 5 changes the shard for 80% of users. Almost all data must be copied while the system is live. - Consistent hashing places shards (and many virtual points per shard) on a ring. Each key goes to the next shard point clockwise.
- A new shard takes over only the keys on its own arcs of the ring; every other key stays put.
- Only 22% of users move, close to the ideal of one in five. Cassandra, DynamoDB and many caches use this idea.
One viral offer melts one shard
Orders are sharded by kitchen. Then one famous kitchen announces a ₹1 biryani offer at 8 pm. Watch the CPU of each shard.
- On a normal day kitchens are spread evenly, so every shard runs at around 40% CPU.
- The viral offer sends a flood of orders for one kitchen id. Every one of them hashes to shard 1, which hits 100% while the others stay calm.
- Adding more shards changes nothing for this key: one key always maps to exactly one shard.
- Split the hot key: append a random suffix 0–9 so the kitchen’s orders spread over ten sub-keys on different shards; reads combine the parts.
- Caching the menu and queueing order writes smooth the burst further. (Load figures are illustrative.)
Partitioning by date: great for reports, bad for writes
Range partitioning by date makes date-range reports fast, but it creates a moving hotspot: every new row lands in the newest partition. Watch the trade-off and the composite-key fix.
- Orders are range-partitioned by month: July, August, September, October.
- A report on October reads only the October partition and skips the rest: partition pruning.
- But every new order belongs to the current month, so all writes hit one partition while the others sit idle. The hotspot moves forward every month.
- A composite key adds a hash bucket in front of the date: new orders spread over four buckets, and a date-range query reads four small pieces instead of one.
- In practice each system is partitioned for its job: the live database by user id, the lake by date, and time-series stores by bucket plus time.
Partitioning in real systems
1. Payment and order databases sharded by user
Large payment apps and marketplaces shard their transactional databases by user or merchant id, so a user’s history and new payments stay on one shard, and add shards as users grow.
2. Kafka topics with many partitions
An orders topic might have 48 partitions keyed by order id, so 48 consumers can process in parallel while each order’s events stay in order within its partition.
3. Lake tables partitioned by date
Almost every analytics table in a lake is partitioned by event date (sometimes also region), so daily jobs and reports scan only the days they need.
4. Distributed caches and NoSQL rings
Cassandra, DynamoDB and distributed caches place keys on a consistent-hash ring, so nodes can be added or replaced with only a small share of keys moving.
Route orders to shards
Orders are routed to 4 shards by a stable hash of the user id. The program prints how many orders land on each shard, then answers one query that has the shard key and one that doesn't. Then try the challenges: range partitioning, choosing a key, mod-N rebalancing, consistent hashing and hot keys.
import hashlib
def shard_for(key, n=4):
return int(hashlib.md5(key.encode()).hexdigest(), 16) % n
orders = [("O1", "U9", "Pune"), ("O2", "U42", "Thane"), ("O3", "U9", "Pune"),
("O4", "U7", "Mumbai"), ("O5", "U42", "Pune"), ("O6", "U13", "Nashik")]
shards = {0: [], 1: [], 2: [], 3: []}
for oid, user, city in orders:
shards[shard_for(user)].append((oid, user, city))
for s, rows in shards.items():
print(f"shard {s}: {rows}")
# With the shard key: ask one shard
target = shard_for("U9")
print("orders of U9 -> shard", target, ":", [r[0] for r in shards[target] if r[1] == "U9"])
# Without the shard key: ask every shard (scatter-gather)
pune = [r[0] for rows in shards.values() for r in rows if r[2] == "Pune"]
print("orders from Pune -> all shards:", pune)return int(hashlib.md5(key.encode()).hexdigest(), 16) % nA stable hash turns the key into a big number;% npicks a shard. (Python's built-inhash()changes between runs, so it can't be used for routing.)shards[shard_for(user)].append((oid, user, city))All orders of one user land on the same shard, because the user id is the partition key.pune = [r[0] for rows in shards.values() for r in rows if r[2] == "Pune"]City is not the shard key, so every shard must be searched: scatter-gather.
Run it on your PC: four real shards with SQLite
This program creates four SQLite database files, routes 20,000 orders to them by a hash of the user id, then compares a query with the shard key (one database) and one without it (all four). Finally it measures how many users would move when a fifth shard is added, with % N and with consistent hashing. Only the standard library is used. Save it as shard_lab.py and run it.
Shards, routing and rebalancing
"""Four real SQLite database files used as shards.
Orders are routed by a hash of the user id. Then we compare a query that uses the
shard key with one that doesn't, and measure how many keys move when a shard is added."""
import bisect, hashlib, os, random, sqlite3
SHARDS = 4
os.makedirs("shards", exist_ok=True)
def h(text):
"""A stable hash (Python's built-in hash() changes between runs)."""
return int(hashlib.md5(text.encode()).hexdigest(), 16)
conns = []
for i in range(SHARDS):
path = f"shards/orders_{i}.db"
if os.path.exists(path):
os.remove(path)
c = sqlite3.connect(path)
c.execute("CREATE TABLE orders (order_id TEXT, user_id TEXT, city TEXT, amount INT)")
conns.append(c)
random.seed(11)
cities = ["Pune", "Mumbai", "Thane", "Nashik"]
for n in range(20_000):
user = f"U{random.randint(1, 3000)}"
shard = h(user) % SHARDS
conns[shard].execute("INSERT INTO orders VALUES (?, ?, ?, ?)",
(f"O{n}", user, random.choice(cities), random.randint(50, 900)))
for c in conns:
c.commit()
print("Rows per shard:", [c.execute("SELECT COUNT(*) FROM orders").fetchone()[0] for c in conns])
# Query 1: by the shard key -> exactly one shard
user = "U42"
shard = h(user) % SHARDS
rows = conns[shard].execute("SELECT COUNT(*), SUM(amount) FROM orders WHERE user_id = ?", (user,)).fetchone()
print(f"\nOrders of {user}: ask shard {shard} only -> {rows[0]} orders, Rs {rows[1]}")
# Query 2: not by the shard key -> ask every shard and merge (scatter-gather)
parts = [c.execute("SELECT COUNT(*) FROM orders WHERE city = 'Pune'").fetchone()[0] for c in conns]
print(f"Orders from Pune: ask all {SHARDS} shards {parts} -> total {sum(parts)}")
# Adding a fifth shard: how many users would have to move?
users = [f"U{i}" for i in range(1, 3001)]
moved_mod = sum(1 for u in users if h(u) % 4 != h(u) % 5)
def ring(nodes, vnodes=100):
points = sorted((h(f"{n}#{v}"), n) for n in nodes for v in range(vnodes))
return [p for p, _ in points], [n for _, n in points]
def owner(key, r):
keys, names = r
i = bisect.bisect(keys, h(key)) % len(keys)
return names[i]
r4, r5 = ring(["S0", "S1", "S2", "S3"]), ring(["S0", "S1", "S2", "S3", "S4"])
moved_ring = sum(1 for u in users if owner(u, r4) != owner(u, r5))
print(f"\nAdding a 5th shard, users that must move:")
print(f" hash % N : {moved_mod} of {len(users)} ({moved_mod / len(users):.0%})")
print(f" consistent hashing: {moved_ring} of {len(users)} ({moved_ring / len(users):.0%})")
# In a terminal: python shard_lab.pyRecorded from a real run with Python 3.13; with the same seed you get the same numbers. Open the shards folder: there are four separate databases, and each user's orders are entirely in one of them. The rebalancing numbers (80% vs 22%) are the ones used in scenario 1 above.
Routing by range, one line at a time
Three orders are placed into quarterly partitions by month. Watch the loop find the first partition whose range contains each month.
Worked examples: real problems, solved and traced in memory
Four partitioning problems, from routing by hash to finding hot keys. Each runs live and is traced.
Which shard holds this user?
Problem statement
Users are sharded across 3 database servers by md5(user_id) % 3. Print the server for each user and how many users each server holds.
U1 … U9, 3 servers
U1 -> server 1
U2 -> server 2
U3 -> server 2
U4 -> server 0
U5 -> server 0
U6 -> server 0
U7 -> server 0
U8 -> server 2
U9 -> server 2
users per server: {0: 4, 1: 1, 2: 4}Approach
- Compute a stable hash of each user id.
- Take the remainder when dividing by 3.
- Count users per server.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
How many date partitions does a query scan?
Problem statement
A lake table has one partition per day for 2026. A query asks for 15–21 October. Count partitions scanned with date partitioning and without (one big folder treated as 1 partition containing everything).
365 daily partitions, 7-day query
partitions scanned: 7 of 365 (days 288-294 of the year) data read: 1.9% of the table
Approach
- Turn each date into its day number in the year.
- Partitions scanned = days in the query range.
- Compare the fraction of data scanned.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
Score candidate partition keys
Problem statement
For each candidate key, count distinct values and the share of rows in the biggest value. A good key has many values and a small biggest share.
8 sample orders, keys: city, user_id, order_id
order_id: 8 distinct, biggest share 12% user_id : 6 distinct, biggest share 25% city : 3 distinct, biggest share 62%
Approach
- For each key, count rows per value.
- Distinct values = number of keys in the count.
- Biggest share = largest count ÷ total rows.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
Detect hot keys from a traffic sample
Problem statement
From a sample of requests, find keys whose traffic is more than 5 times the average key, and suggest how many sub-keys to split each into so that each part is at most 2× average.
sample of 40 requests over 9 keys
K7 is hot: 24 requests vs average 4.4 -> split into 3 sub-keys
Approach
- Count requests per key with
Counter, and the average per key. - A key is hot if its count is more than 5 × average.
- Sub-keys needed = ceil(count ÷ (2 × average)).
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
Check yourself
Quick revision
Lesson 3.2 in one screen
- Partitioning splits data; sharding = horizontal partitions on different servers. Vertical partitioning splits columns.
- Range (ranges of key; fast range queries; newest range is hot) · Hash (even spread; range queries scatter) · Directory (lookup table) · Composite.
- Good partition key: high cardinality, even spread, used by most queries.
- With the key → one shard. Without it → scatter-gather to all shards.
- mod-N resharding moves most keys; consistent hashing + virtual nodes moves ≈ 1/N.
- Hot keys: split with suffixes, cache, or isolate. Date ranges create a moving write hotspot.
- Partitions everywhere: HDFS blocks, Spark partitions, Kafka partitions (order per partition), Cassandra rings, lake date folders.
Interview and viva questions
What is sharding and why is it used?
Sharding is horizontal partitioning of a database across multiple servers: each shard stores a subset of rows chosen by a partition (shard) key. It is used when data size or write throughput exceeds what one server can handle, because each shard stores and serves only part of the data, and shards can be added as the system grows.
Compare range and hash partitioning.
Range partitioning assigns contiguous key ranges to partitions, which makes range queries efficient and data easy to reason about, but can create hotspots (for example all new dates in one partition) and uneven sizes. Hash partitioning assigns rows by a hash of the key, giving an even distribution and avoiding ordering hotspots, but range queries must visit every partition and plain mod-N hashing makes adding partitions expensive.
How do you choose a good partition key?
Choose a key with high cardinality and an even distribution of data and traffic, that appears in most queries so they can be routed to a single partition, and that keeps data used together (such as a user's orders and payments) on the same partition. Avoid low-cardinality keys like city or status and monotonically increasing keys like timestamps for write-heavy tables.
What is consistent hashing?
A technique where both nodes and keys are hashed onto a circular space (a ring); each key belongs to the first node clockwise from its position. When a node is added or removed, only the keys in the affected arc move, about 1/N of all keys, instead of most keys as with hash mod N. Virtual nodes (many ring positions per physical node) even out the load. It is used in Cassandra, DynamoDB and distributed caches.
What problems does sharding introduce?
Queries without the shard key become scatter-gather, joins and transactions across shards are complex and slow, hot keys can overload a shard, rebalancing data when adding shards is operationally hard, and secondary indexes must be designed as local (cheap writes, scattered reads) or global (cheap reads, more expensive writes).
Practice problems
Partitioning exercises, from routing a key to sizing Kafka partitions. The last two are business tasks.
Shard for an id
Numeric customer ids are sharded with id % 6. Print the shard of each id.
101, 256, 999, 1200
101 -> shard 5 256 -> shard 4 999 -> shard 3 1200 -> shard 0
Show solution and explanation
for cid in [101, 256, 999, 1200]:
print(cid, "-> shard", cid % 6)How it works: The remainder of division picks the shard.
Range lookup
Partitions: P0 for ids 1–1000, P1 for 1001–2000, P2 for 2001–3000. Print the partition of id 1500.
1500
P1
Show solution and explanation
ranges = [("P0", 1, 1000), ("P1", 1001, 2000), ("P2", 2001, 3000)]
print(next(p for p, lo, hi in ranges if lo <= 1500 <= hi))How it works: Find the range that contains the id.
Rows per partition
Count orders per city partition.
6 orders
{'Pune': 2, 'Mumbai': 3, 'Thane': 1}Show solution and explanation
cities = ["Pune", "Mumbai", "Pune", "Thane", "Mumbai", "Mumbai"]
counts = {}
for c in cities:
counts[c] = counts.get(c, 0) + 1
print(counts)How it works: Uneven counts reveal a poor partition key.
Skew ratio
Given rows per shard, print the skew (largest ÷ average) and whether it is above 1.5.
[120, 95, 300, 110]
1.92 unbalanced
Show solution and explanation
rows = [120, 95, 300, 110]
skew = max(rows) / (sum(rows) / len(rows))
print(round(skew, 2), "unbalanced" if skew > 1.5 else "ok")How it works: A skew of 1.0 is perfect; large values mean one shard does most of the work.
Scatter-gather merge
Each shard returns its top-2 orders by amount. Merge them into the global top-3.
3 shards
[('O5', 990), ('O1', 900), ('O3', 880)]Show solution and explanation
shard_results = [[("O1", 900), ("O7", 650)], [("O3", 880), ("O9", 300)], [("O5", 990), ("O2", 410)]]
merged = sorted([r for res in shard_results for r in res], key=lambda r: -r[1])
print(merged[:3])How it works: The router combines partial answers; each shard only needs to send its own top results.
Keys moved with mod-N
For numeric ids 1–1000, print how many change shard when going from 5 to 6 shards with id % N.
ids 1–1000
831 of 1000 move
Show solution and explanation
moved = sum(1 for i in range(1, 1001) if i % 5 != i % 6)
print(moved, "of 1000 move")How it works: Most keys move, which is why systems prefer consistent hashing or fixed logical partitions.
Logical to physical shards
1,024 logical shards are spread over 4 servers; one server is added. Move whole logical shards so all 5 servers have nearly equal counts, and print how many logical shards moved.
1024 logical, 4 → 5 servers
target per server: [205, 205, 205, 205, 204] logical shards moved: 204 (20%)
Show solution and explanation
logical = 1024
before = [logical // 4] * 4
target = [logical // 5 + (1 if i < logical % 5 else 0) for i in range(5)]
moved = sum(max(0, b - t) for b, t in zip(before, target[:4]))
print("target per server:", target)
print("logical shards moved:", moved, f"({moved / logical:.0%})")How it works: With many small logical shards, growth moves only the minimum amount of data: about one fifth here.
Kafka partitions for throughput
A topic must handle 60 MB/s at peak. One consumer processes 4 MB/s. Plan 50% headroom. How many partitions (and consumers) are needed?
60 MB/s, 4 MB/s per consumer, +50%
23 partitions
Show solution and explanation
import math
print(math.ceil(60 * 1.5 / 4), "partitions")How it works: Each partition is read by at most one consumer in a group, so partitions set the maximum parallelism.
Shards for a growing table
The orders table is 6 TB and grows 250 GB a month. Each shard server should hold at most 2 TB. How many shards are needed now and in 2 years?
6 TB, +250 GB/month, 2 TB per shard
now: 3 shards | in 2 years: 6 shards
Show solution and explanation
import math
now = 6
later = now + 0.25 * 24
print("now:", math.ceil(now / 2), "shards | in 2 years:", math.ceil(later / 2), "shards")How it works: Plan the shard count (and the rebalancing method) for growth, not just for today.
Real-world scenario questions (asked in company interviews)
Partitioning decisions from real systems and interviews. Decide your answer first, then open the model answer.
Choose a shard key for a chat app
A chat app stores messages. The main screens are “messages in this conversation, newest first” and “my conversations”. Messages reach crores per day. How would you partition the messages table?
The interviewer is checking: can you choose a key from the access pattern and spot hotspots?
Show a strong answer
Partition by conversation_id (all messages of a chat together, so one query reads one partition), clustered by time descending. Bound partition size for huge group chats by adding a time bucket: (conversation_id, month). Keep “my conversations” in a separate table partitioned by user_id. Avoid partitioning by date (write hotspot) or by user for messages (a conversation would span many partitions).
Avoid: picking a key without saying which queries it serves.
We need to go from 8 to 12 shards
A sharded MySQL setup uses user_id % 8. Growth means you need 12 shards. How do you migrate safely?
The interviewer is checking: do you understand the cost of mod-N resharding and safe migration steps?
Show a strong answer
- Recognise that changing the modulus moves most users. Consider moving to a directory-based or consistent-hash scheme, or to many logical shards (for example 1,024) mapped onto physical servers, so future growth moves whole logical shards.
- Copy data to the new layout in the background, then dual-write new changes to old and new locations.
- Verify counts and checksums per user range.
- Switch reads gradually, then writes; keep a rollback path.
- Finally, clean up the old copies.
Avoid: changing the modulus in production and copying everything in one big bang.
Order events are processed out of order
Consumers sometimes process “order delivered” before “order placed” for the same order. The topic has 12 partitions and producers don’t set a message key.
The interviewer is checking: do you know how Kafka partitioning affects ordering?
Show a strong answer
Without a key, messages are spread across partitions, and Kafka guarantees order only within one partition. Set the message key to order_id so all events for an order go to the same partition and are consumed in order by one consumer. Choose enough partitions for the needed parallelism, and watch for hot keys. Changing partition count later remaps keys, so plan it up front.
Avoid: adding a sleep in consumers or using a single partition for everything.
“What is the difference between partitioning and replication?”
Interviewers often ask this to check basic distributed-systems vocabulary.
The interviewer is checking: can you separate splitting data from copying data?
Show a strong answer
Partitioning (sharding) splits data into parts so each node stores and processes only a subset; it increases capacity and throughput. Replication stores copies of the same data on several nodes; it increases availability, durability and read capacity. Real systems use both: each partition is replicated (for example HDFS blocks with 3 replicas, Kafka partitions with a replication factor, Cassandra partitions on several nodes).
Avoid: using the two words as if they mean the same thing.