Partitioning and sharding: splitting data across machines

When one table is too big for one machine, you split it: that is partitioning, and when the parts live on different servers, sharding. In this lesson you choose a partition key, route queries to one shard or all of them, see why adding a shard can move 80% of your data (and how consistent hashing cuts that to about 20%), and watch a viral offer overload a single shard.

Module 3 · Distributed Systems Concepts⏱ 30 minBeginner → AdvancedRuns in your browser (Python)3 real-time scenariosFour real shards on your PC9 practice problems
1 The story

“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.

orders40 crore rowsone server: fullsplitrouterhash(user_id) % 4→ shardshard 0: users with hash % 4 = 0shard 1: users with hash % 4 = 1shard 2: users with hash % 4 = 2shard 3: users with hash % 4 = 3 One table too big for one machine becomes four smaller tables on four machines. myphatshala.in
Kabir

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?

Arjun Sir

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.

Riya

Like a library that puts books A to F on one shelf and G to M on another?

Arjun Sir

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.

Kabir

And if we choose the wrong key?

Arjun Sir

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.

2 Definition

What are partitioning and sharding?

Formal definition (write this in exams)

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.

In simple words

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

Partition / shardOne piece of the data. “Shard” is used when the pieces are on different database servers.
Partition keyThe field whose value decides which partition a row goes to.
CardinalityThe number of distinct values a field has. User id: crores. City: a few dozen.
RouterThe component that maps a key to its shard.
Scatter-gatherSending a query to every shard and merging the answers.
RebalancingMoving data between shards when machines are added or removed.
Consistent hashingA hashing scheme where adding or removing a shard moves only a small fraction of keys.
Hot partition / hot keyA partition or key that receives far more traffic than others.

Partitioning across the big data stack

HDFSfiles split into 128 MB blocks spread over DataNodes
SparkDataFrames split into partitions processed in parallel; shuffles repartition by key
Kafkatopics split into partitions; order is kept only within a partition; partition count limits consumer parallelism
Cassandra / DynamoDBpartition key hashed onto a ring of nodes
Data lake / warehousetables partitioned by date or region folders; queries skip partitions they don’t need
Sharded SQLMySQL/PostgreSQL shards behind a router (for example Vitess or Citus)

Partitioning at three levels

Pick your level. New to this? Start at Beginner and climb.

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.

3 Types and ways to use it

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 ranges

Hash partitioning

hash(key) decides the partition. Even spread, no hotspots from ordering; range queries must hit every partition.

Even spread

Directory / list

A lookup table maps keys (or groups like regions) to partitions. Very flexible; the directory must be fast and highly available.

Lookup table

Composite

Combine methods: hash on user id to pick a shard, then range on date inside it. Common in Cassandra-style keys.

Hash + range

Side by side

RangeHashDirectory
How a row is placedWhich range its key falls inhash(key) % N or on a ringLook up the key in a table
Range queriesEfficient (few partitions)Must ask all partitionsDepends on the mapping
BalanceCan be uneven; newest range is hotEven for good keysCan be tuned by hand
Adding a nodeSplit a rangemod-N moves most keys; consistent hashing moves ~1/NUpdate the directory
ExampleOrders by monthUsers by hash(user_id)Tenants mapped to shards

Choosing a partition key

Candidate keyDistinct valuesSpreadUsed in most queries?Verdict
cityabout 50Very uneven (metros dominate)SometimesPoor
order_date365 per yearAll writes hit todayFor reports, yesGood for analytics, poor for live writes
user_idcroresEvenYes (my orders, my payments)Good for the app
order_idbillionsEvenOnly for single-order look-upsGood 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.

4 Step by step

Sharding an orders table

  1. 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 user_id.

    The key decides everything else: balance, which queries are fast, and which queries must visit every shard.
  2. Hash partitioning spreads users evenly

    import 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 2
    A stable hash (here MD5; never Python’s hash(), which changes between runs) turns each user id into a big number; % 4 picks one of four shards. Similar ids land on different shards, so load spreads evenly.
  3. Range partitioning keeps dates together

    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 -> P0
    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.
  4. The trouble with % N when you grow

    import 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 move
    Going from 4 to 5 shards changes hash % N for most keys, so most of the data would have to move. Consistent hashing reduces this to roughly one fifth (see scenario 1).
  5. Directory partitioning for special cases

    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-D
    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.
  6. Queries 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.
5 See it happen

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.

What users see, and how the router finds the data 6 steps
What you see My ordersLoading…My ordersO1043₹420O1042₹180O1041₹250O1040₹90My ordersO1043₹420O1042₹180O1041₹250O1040₹90loaded in 8 msSupport consoleSearch: all orders from Pune todaysearching…Support consoleSearch: all orders from Pune today41,236 orderstook 2.4 sSupport consoleSearch: all orders from Pune today41,236 ordersnow 0.3 s (from the analytics copy) myphatshala.in
Behind the screen routershard 0users Ashard 1users Bshard 2users Cshard 3users Drequest: orders of user U9The router receives the request with user_id = U9.routershard 0users Ashard 1users Bshard 2users Cshard 3users Dhash(U9) % 4 = 3The key tells the router exactly which shard to ask.routershard 0users Ashard 1users Bshard 2users Cshard 3users DOne shard answers with 4 rows. Other shards are untouched.routershard 0users Ashard 1users Bshard 2users Cshard 3users Dquery: city = Pune (no user_id)The query has no shard key.routershard 0users Ashard 1users Bshard 2users Cshard 3users Dscatter to all 4, gather, mergeEvery shard scans its data; the router merges the answers.app shardsby user_id(live orders)lake tableby date, city(analytics copy)Same data, partitioned twice:by user for the app, by date for analysis. myphatshala.in
6 Real-world use

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 1

Adding 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.

Rebalancing 5 steps
Animated example 3,000 users on 4 shardsTraffic grows. We add a 5th shard.How many users must move to a different shard?hash % N: 4 → 580% of users change shard (2,412 of 3,000).Almost the whole database must be copied.hash ringConsistent hashing: shards and keys sit on a ring;each key belongs to the next shard clockwise.new shard S4The new shard takes over only one arc of the ring.Keys elsewhere stay where they are.consistent hashing: 4 → 5Only 22% of users move (653 of 3,000).Virtual nodes keep each shard’s share even. myphatshala.in
Real-time scenario 2

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.

Hot key 5 steps
Animated example CPU per shardshard 040%shard 142%shard 238%shard 341%Normal day: orders spread evenly by kitchen_id.CPU per shardshard 044%shard 1100%shard 240%shard 343%A famous biryani kitchen runs a ₹1 offer. Its ordersall hash to shard 1, which is overloaded: a hot key.CPU per shardshard 044%shard 1100%shard 240%shard 343%Adding shards doesn’t help: one key always mapsto one shard, however many shards there are.CPU per shardshard 062%shard 164%shard 261%shard 363%key = kitchen_id + "#" + random(0–9)Fix: split the hot key into 10 sub-keysspread over shards; readers combine the parts.CPU per shardshard 045%shard 147%shard 244%shard 346%Plus: cache the menu, and queue order writesso short bursts are smoothed out. myphatshala.in
Real-time scenario 3 · advanced

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.

Range hotspots 5 steps
Animated example JulpartitionAugpartitionSeppartitionOctpartitionOrders range-partitioned by monthReports love this: “October revenue” reads one partition.JulpartitionAugpartitionSeppartitionOctpartitionReport for October → 1 of 4 partitionsPartition pruning: the other months are skipped.JulpartitionAugpartitionSeppartitionOctpartitionBut all new orders go to OctoberWrites: 100% on the newest partition (a moving hotspot).Older partitions sit idle for writes.bucket 0Oct, hashedbucket 1Oct, hashedbucket 2Oct, hashedbucket 3Oct, hashedComposite: hash bucket + dateKey = (hash(order_id) % 4, date). New orders spreadover 4 buckets; a date range reads 4 small pieces.Choose per system• live order database: hash by user_id (even writes)• lake table: partition by date (prune reports)• time series store: bucket + time (both)The same data, partitioned differently for each job. myphatshala.in

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.

Request with user_id→Router → one shard→Fast, isolated

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.

Producers→48 partitions by key→48 consumers
date=2026-10-19

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.

Daily data→date=… folders→Pruned queries

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.

Key→Position on ring→Owner node
7 Run the code

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)
shard 0: [('O2', 'U42', 'Thane'), ('O5', 'U42', 'Pune')] shard 1: [('O1', 'U9', 'Pune'), ('O3', 'U9', 'Pune'), ('O6', 'U13', 'Nashik')] shard 2: [] shard 3: [('O4', 'U7', 'Mumbai')] orders of U9 -> shard 1 : ['O1', 'O3'] orders from Pune -> all shards: ['O5', 'O1', 'O3']
  • return int(hashlib.md5(key.encode()).hexdigest(), 16) % nA stable hash turns the key into a big number; % n picks a shard. (Python's built-in hash() 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.py
Rows per shard: [5189, 5085, 4918, 4808] Orders of U42: ask shard 0 only -> 4 orders, Rs 1927 Orders from Pune: ask all 4 shards [1303, 1298, 1246, 1171] -> total 5018 Adding a 5th shard, users that must move: hash % N : 2412 of 3000 (80%) consistent hashing: 653 of 3000 (22%)

Recorded 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.

8 Line by line

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.

Example 1BeginnerDatabase · routing

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.

Input
U1 … U9, 3 servers
Expected output
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

  1. Compute a stable hash of each user id.
  2. Take the remainder when dividing by 3.
  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.

Example 2IntermediateData lake · pruning

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).

Input
365 daily partitions, 7-day query
Expected output
partitions scanned: 7 of 365 (days 288-294 of the year)
data read: 1.9% of the table

Approach

  1. Turn each date into its day number in the year.
  2. Partitions scanned = days in the query range.
  3. 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.

Example 3IntermediateDesign · key choice

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.

Input
8 sample orders, keys: city, user_id, order_id
Expected output
order_id: 8 distinct, biggest share 12%
user_id : 6 distinct, biggest share 25%
city    : 3 distinct, biggest share 62%

Approach

  1. For each key, count rows per value.
  2. Distinct values = number of keys in the count.
  3. 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.

Example 4AdvancedOperations · hot keys

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.

Input
sample of 40 requests over 9 keys
Expected output
K7 is hot: 24 requests vs average 4.4 -> split into 3 sub-keys

Approach

  1. Count requests per key with Counter, and the average per key.
  2. A key is hot if its count is more than 5 × average.
  3. 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.

9 Quiz and revision

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.

P1Easy

Shard for an id

Numeric customer ids are sharded with id % 6. Print the shard of each id.

Input
101, 256, 999, 1200
Expected output
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)
101 -> shard 5 256 -> shard 4 999 -> shard 3 1200 -> shard 0

How it works: The remainder of division picks the shard.

P2Easy

Range lookup

Partitions: P0 for ids 1–1000, P1 for 1001–2000, P2 for 2001–3000. Print the partition of id 1500.

Input
1500
Expected output
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))
P1

How it works: Find the range that contains the id.

P3Easy

Rows per partition

Count orders per city partition.

Input
6 orders
Expected output
{'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)
{'Pune': 2, 'Mumbai': 3, 'Thane': 1}

How it works: Uneven counts reveal a poor partition key.

P4Medium

Skew ratio

Given rows per shard, print the skew (largest ÷ average) and whether it is above 1.5.

Input
[120, 95, 300, 110]
Expected output
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")
1.92 unbalanced

How it works: A skew of 1.0 is perfect; large values mean one shard does most of the work.

P5Medium

Scatter-gather merge

Each shard returns its top-2 orders by amount. Merge them into the global top-3.

Input
3 shards
Expected output
[('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])
[('O5', 990), ('O1', 900), ('O3', 880)]

How it works: The router combines partial answers; each shard only needs to send its own top results.

P6Advanced

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.

Input
ids 1–1000
Expected output
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")
831 of 1000 move

How it works: Most keys move, which is why systems prefer consistent hashing or fixed logical partitions.

P7Advanced

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.

Input
1024 logical, 4 → 5 servers
Expected output
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%})")
target per server: [205, 205, 205, 205, 204] logical shards moved: 204 (20%)

How it works: With many small logical shards, growth moves only the minimum amount of data: about one fifth here.

P8Business problem

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?

Input
60 MB/s, 4 MB/s per consumer, +50%
Expected output
23 partitions
Show solution and explanation
import math
print(math.ceil(60 * 1.5 / 4), "partitions")
23 partitions

How it works: Each partition is read by at most one consumer in a group, so partitions set the maximum parallelism.

P9Business problem

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?

Input
6 TB, +250 GB/month, 2 TB per shard
Expected output
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")
now: 3 shards | in 2 years: 6 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.

System design · partitioningFresher

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.

Operations · rebalancing1–3 years

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
  1. 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.
  2. Copy data to the new layout in the background, then dual-write new changes to old and new locations.
  3. Verify counts and checksums per user range.
  4. Switch reads gradually, then writes; keep a rollback path.
  5. Finally, clean up the old copies.

Avoid: changing the modulus in production and copying everything in one big bang.

Streaming · Kafka1–3 years

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.

Fundamentals · interviewFresher

“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.