Database Sharding & Partitioning Strategies: Range, Hash, and Dynamic Rebalancing
Deconstructing horizontal sharding keys, range vs hash partitioning, cross-shard joins, and dynamic split rebalancing
Part 5 in Series — Catch up on the previous article: Consistent Hashing & Virtual Nodes: Distributing Keys Without Mass Resharding (Part 4) before diving into this post.
Why You Need This in Real Life
When a single relational database table reaches 500 million rows (250GB in index and page data), database query performance degrades rapidly:
- Buffer pool cache hit ratios drop from to because index B+ Trees exceed available server RAM.
ALTER TABLEschema migration commands take 14 hours and lock write operations.- Disk I/O bottlenecks choke concurrent transactions.
To scale beyond the hardware limits of a single machine, database architectures must shard (horizontally partition) tables across multiple database instances.
However, choosing the wrong sharding key (e.g., sharding a multi-tenant SaaS application by created_at timestamp) creates severe production operational traps: all incoming writes hit a single “hot” shard containing today’s date, while older shards sit idle.
To design scalable database architectures, you must master Range Partitioning, Hash Sharding, Cross-Shard Join Mitigation, and Dynamic Shard Rebalancing.
Part 1: Horizontal Sharding vs Vertical Partitioning
- Vertical Partitioning: Splitting a wide table by columns into smaller tables (e.g., moving large
textorblobcolumns to a separateuser_profiles_blobtable). - Horizontal Sharding: Splitting a table by rows across independent database servers. Every shard shares the identical schema, but holds a distinct subset of rows.
[ Single 500M Row Table ]
|
+---------------------------------+---------------------------------+
| | |
v v v
[ Shard 1 (Node 1) ] [ Shard 2 (Node 2) ] [ Shard 3 (Node 3) ]
Rows 1 to 166M Rows 166M to 333M Rows 333M to 500M
Part 2: Sharding Strategies Breakdown
1. Range-Based Partitioning
Rows are assigned to shards based on contiguous value ranges of the sharding key (e.g., user_id 1–100,000 on Shard A, 100,001–200,000 on Shard B).
- Advantages: Range queries (
SELECT * WHERE user_id BETWEEN 50 AND 150) hit a single shard. - Drawbacks: Sequential primary keys (
AUTO_INCREMENT) or timestamp keys create monotonic write hotspots—all new writes target the newest range shard.
2. Hash-Based Partitioning
Rows are assigned to shards using a hash function over the sharding key:
- Advantages: Uniform write and read distribution across all shard nodes ( load variance).
- Drawbacks: Range queries (
WHERE created_at BETWEEN X AND Y) cannot be routed to a single shard; they must be broadcast to all shards (Scatter-Gather query pattern).
3. Entity/Tenant-Based Sharding
In multi-tenant SaaS platforms, all records belonging to a specific tenant (tenant_id = 42) are co-located on the same physical shard.
- Advantages: Eliminates cross-shard transactions and joins for single-tenant queries.
- Drawbacks: The “Celebrity / Large Tenant Problem”—a giant enterprise tenant can overload its assigned shard.
+-----------------------------------------------------------------------------+
| Sharding Strategy Matrix |
| |
| Strategy | Primary Strengths | Critical Vulnerability |
| ----------------+------------------------------+---------------------------|
| Range | Efficient range scans | Monotonic write hotspots |
| Hash | Uniform load distribution | Scatter-Gather range scans|
| Entity/Tenant | Fast single-tenant joins | Large tenant hotspots |
+-----------------------------------------------------------------------------+
Part 3: The Cross-Shard Operations Traps
Sharding solves storage scale, but introduces three major distributed architecture complexities:
1. Cross-Shard Joins
Executing SELECT * FROM orders JOIN users ON orders.user_id = users.id across shards located on different database servers requires fetching raw tables over the network and executing a Hash Join in the application routing tier, introducing high latency.
- Mitigation: Denormalize data, use DTO projections, or co-locate related tables on the same shard using a shared
user_idcomposite sharding key.
2. Global Unique Primary Key Generation
Standard database AUTO_INCREMENT or SERIAL sequences fail across shards because independent shards will generate conflicting primary key IDs (id = 1 on Shard 1 and id = 1 on Shard 2).
- Mitigation: Use 64-bit Snowflake IDs (Timestamp + Datacenter ID + Worker ID + Sequence Counter) or UUIDv7.
3. Distributed Transactions
Updating rows across two separate shards requires Two-Phase Commit (2PC) or Saga patterns, increasing transaction latency by 10x.
Part 4: Dynamic Shard Splitting & Rebalancing
As data volume grows, fixed shard counts become insufficient. Systems like Google Spanner, CockroachDB, and MongoDB use Dynamic Shard Splitting:
1. Shard A reaches capacity limit (100GB threshold)
[ Shard A: Keys 1 to 1000 ]
2. Router initiates Split at Key 500
[ Shard A1: Keys 1 to 500 ] <---> [ Shard A2: Keys 501 to 1000 ]
3. Router updates Shard Map Metadata in background
Steps in Dynamic Rebalancing
- Threshold Trigger: A monitoring agent detects a shard exceeding disk capacity () or IOPS limits.
- Split Point Selection: The engine selects the median key to divide the range into two equal halves.
- Background Copy: The new target node copies data pages in the background while the parent shard accepts live writes using a Change Data Capture (CDC) write log.
- Metadata Swap: An atomic metadata update in the routing service (e.g., via etcd or ZooKeeper) redirects client traffic to the new shard boundaries.
Part 5: Java Snowflake ID Generator for Sharded Systems
package com.example.sharding;
public class SnowflakeIdGenerator {
private static final long START_EPOCH = 1704067200000L; // 2024-01-01 UTC
private static final long WORKER_ID_BITS = 5L;
private static final long DATACENTER_ID_BITS = 5L;
private static final long SEQUENCE_BITS = 12L;
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException("Worker ID out of range");
}
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException("Datacenter ID out of range");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backward! Refusing to generate ID");
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - START_EPOCH) << TIMESTAMP_LEFT_SHIFT) |
(datacenterId << DATACENTER_ID_SHIFT) |
(workerId << WORKER_ID_SHIFT) |
sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
}
Next Steps
Now that we understand database sharding and dynamic rebalancing, we will explore Gossip Protocols and cluster membership in Part 6.
References & Further Reading
- Kleppmann, M. (2017). Designing Data-Intensive Applications (Chapter 6: Partitioning). O’Reilly Media.
- MongoDB Inc. MongoDB Sharded Cluster Architecture and Balancer Mechanics. MongoDB Docs.
- Apache Software Foundation. Apache Cassandra Architecture: Data Partitioning Ring. Cassandra Docs.
Part 6: Gossip Protocols & Cluster Membership: How Decentralized Nodes Maintain Topology
Continue to Part 6 →