Data partitioning and sharding strategies including hash-based, range-based, consistent hashing, and rebalancing
Scope: Partitioning strategies, consistent hashing, rebalancing, hotspots, practical implementations Lines: ~280 Last Updated: 2025-10-27 Format Version: 1.0 (Atomic)
Activate this skill when:
Problem: Single node can't handle all data/load
Solution: Split data across multiple nodes
Benefits:
✅ Scalability (more nodes = more capacity)
✅ Performance (parallel queries)
✅ Fault isolation (failure affects subset)
Partitioning: General term for splitting data Sharding: Partitioning in distributed databases
Often used interchangeably
Approach: Hash key to determine partition
import hashlib
class HashPartitioner:
"""Hash-based partitioning"""
def __init__(self, num_partitions):
self.num_partitions = num_partitions
self.partitions = [[] for _ in range(num_partitions)]
def get_partition(self, key):
"""Determine partition for key"""
hash_val = int(hashlib.md5(key.encode()).hexdigest(), 16)
return hash_val % self.num_partitions
def write(self, key, value):
"""Write to appropriate partition"""
partition_id = self.get_partition(key)
self.partitions[partition_id].append((key, value))
def read(self, key):
"""Read from appropriate partition"""
partition_id = self.get_partition(key)
for k, v in self.partitions[partition_id]:
if k == key:
return v
return None
# Usage
partitioner = HashPartitioner(num_partitions=4)
partitioner.write('user:123', {'name': 'Alice'})
partitioner.write('user:456', {'name': 'Bob'})
print(partitioner.read('user:123')) # {'name': 'Alice'}
Pros:
Cons:
Approach: Assign key ranges to partitions
class RangePartitioner:
"""Range-based partitioning"""
def __init__(self, ranges):
# ranges = [(min, max, partition_id), ...]
self.ranges = sorted(ranges, key=lambda x: x[0])
self.partitions = {}
def get_partition(self, key):
"""Find partition for key"""
for min_key, max_key, partition_id in self.ranges:
if min_key <= key < max_key:
return partition_id
raise KeyError(f"No partition for key {key}")
def range_query(self, start_key, end_key):
"""Query range of keys"""
results = []
# Find all relevant partitions
for min_key, max_key, partition_id in self.ranges:
if max_key >= start_key and min_key < end_key:
# This partition contains relevant data
results.extend(self._query_partition(partition_id, start_key, end_key))
return results
# Usage
partitioner = RangePartitioner([
('A', 'G', 0), # Partition 0: A-F
('G', 'M', 1), # Partition 1: G-L
('M', 'S', 2), # Partition 2: M-R
('S', 'Z', 3), # Partition 3: S-Z
])
Pros:
Cons:
Approach: Hash keys and nodes onto ring, walk clockwise
import hashlib
import bisect
class ConsistentHashing:
"""Consistent hashing with virtual nodes"""
def __init__(self, num_virtual_nodes=150):
self.num_virtual_nodes = num_virtual_nodes
self.ring = [] # Sorted list of (hash, node_id)
self.nodes = set()
def _hash(self, key):
"""Hash key to position on ring"""
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_node(self, node_id):
"""Add node to ring"""
self.nodes.add(node_id)
# Add virtual nodes
for i in range(self.num_virtual_nodes):
virtual_key = f"{node_id}:{i}"
hash_val = self._hash(virtual_key)
bisect.insort(self.ring, (hash_val, node_id))
def remove_node(self, node_id):
"""Remove node from ring"""
self.nodes.discard(node_id)
# Remove virtual nodes
self.ring = [(h, n) for h, n in self.ring if n != node_id]
def get_node(self, key):
"""Find node responsible for key"""
if not self.ring:
return None
hash_val = self._hash(key)
# Find first node clockwise
idx = bisect.bisect_right(self.ring, (hash_val, ''))
if idx == len(self.ring):
idx = 0
return self.ring[idx][1]
# Usage
ch = ConsistentHashing()
ch.add_node('node1')
ch.add_node('node2')
ch.add_node('node3')
print(ch.get_node('user:123')) # node2
print(ch.get_node('user:456')) # node1
# Add node - minimal keys moved
ch.add_node('node4')
Pros:
Cons:
Celebrity effect:
User "celebrity" has 10M followers
All queries for "celebrity" → Same partition → Overload
class AdaptivePartitioner:
"""Split partitions that become hot"""
def __init__(self):
self.partitions = {}
self.access_counts = {}
def access(self, key):
self.access_counts[key] = self.access_counts.get(key, 0) + 1
# Split if too hot
if self.access_counts[key] > 10000:
self._split_hot_key(key)
def _split_hot_key(self, key):
"""Replicate hot key to multiple partitions"""
# Create replicas
for i in range(3):
replica_key = f"{key}:replica:{i}"
self.partitions[replica_key] = self.partitions[key]
def read_with_randomization(key):
"""Read from random replica of hot key"""
replicas = get_replicas(key)
replica = random.choice(replicas)
return replica.read(key)
Triggers:
- Node added (scale out)
- Node removed (failure or scale down)
- Load imbalance detected
1. Stop-the-World (simple but downtime):
def rebalance_stop_the_world(old_partitions, new_num_partitions):
"""Rebalance with downtime"""
# 1. Stop writes
stop_writes()
# 2. Redistribute data
new_partitions = redistribute(old_partitions, new_num_partitions)
# 3. Resume writes
resume_writes()
return new_partitions
2. Online Rebalancing (no downtime):
class OnlineRebalancer:
"""Rebalance without downtime"""
def rebalance(self, target_partitions):
# 1. Create new partitions (in parallel with old)
new_partitions = self._create_partitions(target_partitions)
# 2. Start dual-writing (to both old and new)
self._enable_dual_write()
# 3. Copy existing data to new partitions
self._copy_data(new_partitions)
# 4. Switch reads to new partitions
self._switch_reads(new_partitions)
# 5. Stop dual-writing, remove old partitions
self._disable_dual_write()
self._remove_old_partitions()
Partition by user_id, but want to query by email
class LocalIndexPartitioner:
"""Local secondary index per partition"""
def __init__(self, num_partitions):
self.partitions = [
{'data': {}, 'email_index': {}}
for _ in range(num_partitions)
]
def write(self, user_id, email, data):
"""Write with local index"""
partition_id = hash(user_id) % len(self.partitions)
partition = self.partitions[partition_id]
partition['data'][user_id] = data
partition['email_index'][email] = user_id
def query_by_email(self, email):
"""Scatter-gather across all partitions"""
for partition in self.partitions:
if email in partition['email_index']:
user_id = partition['email_index'][email]
return partition['data'][user_id]
return None
class GlobalIndexPartitioner:
"""Separate global secondary index"""
def __init__(self, num_partitions):
self.data_partitions = [{} for _ in range(num_partitions)]
self.global_email_index = {} # email → (partition_id, user_id)
def write(self, user_id, email, data):
"""Write to data partition and global index"""
partition_id = hash(user_id) % len(self.data_partitions)
self.data_partitions[partition_id][user_id] = data
self.global_email_index[email] = (partition_id, user_id)
def query_by_email(self, email):
"""Direct lookup via global index"""
if email in self.global_email_index:
partition_id, user_id = self.global_email_index[email]
return self.data_partitions[partition_id][user_id]
return None
// Enable sharding
sh.enableSharding("mydb")
// Shard collection by user_id (hash)
sh.shardCollection("mydb.users", {user_id: "hashed"})
// Or by range
sh.shardCollection("mydb.orders", {order_date: 1})
-- Partition key determines data distribution
CREATE TABLE users (
user_id UUID,
name TEXT,
PRIMARY KEY (user_id) -- Partition key
);
-- Composite partition key
CREATE TABLE user_posts (
user_id UUID,
post_date DATE,
post_id UUID,
content TEXT,
PRIMARY KEY ((user_id, post_date), post_id) -- Partition key: (user_id, post_date)
);
| Strategy | Distribution | Range Queries | Rebalancing | Hotspots | |----------|--------------|---------------|-------------|----------| | Hash | ✅ Even | ❌ Hard | ⚠️ Rehash all | ✅ Rare | | Range | ⚠️ Uneven | ✅ Easy | ✅ Easy | ❌ Common | | Consistent Hash | ✅ Even | ❌ Hard | ✅ Minimal | ✅ Rare |
distributed-systems-replication-strategies - Data replicationdistributed-systems-consensus-raft - Consensus for configurationdistributed-systems-eventual-consistency - Consistency modelsdistributed-systems-distributed-locks - Coordination primitivesLast Updated: 2025-10-27