Hello! Welcome back to our module on Parallel and Distributed Databases.
In our last lesson, we explored the foundational architectures for large-scale data systems. We concluded that the shared-nothing architecture, where each node has its own CPU, memory, and disk, offers the best path to massive scalability. We also identified its primary engineering challenge: if data is spread across many independent machines, how do you decide which data lives on which machine?
This lesson directly answers that question. Our goal is to implement hash-based and range-based data partitioning (sharding) strategies. Partitioning, often called sharding, is the core mechanism for distributing data in a shared-nothing system. Mastering these techniques is fundamental to building scalable, high-performance distributed databases.
1. What is Data Partitioning?
Before we dive into the specific strategies, let's establish what partitioning is and what we're trying to achieve. In essence, partitioning is the process of taking a large database table and breaking it into smaller, more manageable pieces called partitions or shards, which are then spread across multiple database nodes.
Google SWE teaches systems design | EP5: Database sharding/partitioning
For a great overview from an industry perspective, let's watch this short clip from a Google Software Engineer. He explains the 'what' and 'why' of partitioning.
Watch from 01:07 to 02:33. Pay close attention to the two main objectives of partitioning: achieving an even distribution of both data and query load.
As the video highlights, the ideal partitioning scheme avoids hotspots—nodes that receive a disproportionate amount of data or queries. This process of splitting a table's rows into subsets across different machines is more formally known as horizontal partitioning.

Now, let's get into the implementation details of the two most common strategies.
2. Strategy 1: Range-based Partitioning
This is the most straightforward approach to partitioning. We choose a partitioning key (e.g., user ID, product SKU, creation timestamp) and divide its possible values into contiguous ranges. Each range is then assigned to a specific partition.

How it Works
Imagine an orders table with an order_date column as the partitioning key. We could decide:
- Shard 1: Orders from 2022
- Shard 2: Orders from 2023
- Shard 3: Orders from 2024
To route a query, the system needs to know which shard holds the data. This is typically done using a simple metadata lookup table.
// Metadata mapping ranges to shard IDs
RangeShardMap = {
(-∞, '2023-01-01'): Shard1,
['2023-01-01', '2024-01-01'): Shard2,
['2024-01-01', +∞): Shard3
}
Implementation Logic
From an implementation standpoint, finding the correct shard for a given key is a simple lookup. Given your background in data structures, you can think of this lookup table as being implemented with a sorted list or a balanced binary search tree for efficient searching.
// Pseudocode for finding a shard for a given key
class RangePartitioner {
// Stores range boundaries and corresponding shard IDs.
// E.g., a sorted list of pairs: (upper_bound, shard_id)
private SortedMap<Key, ShardID> rangeMap;
public ShardID findShardForKey(Key k) {
// Find the first range boundary that is greater than the key.
// The shard associated with that boundary is our target.
for (Entry<Key, ShardID> entry : rangeMap.entrySet()) {
if (k.compareTo(entry.getKey()) < 0) {
return entry.getValue();
}
}
// Should not happen if ranges are configured correctly
return null;
}
}
Trade-offs
Google SWE teaches systems design | EP5: Database sharding/partitioning
Let's hear about the pros and cons of range partitioning from a practical viewpoint.
Watch from 02:53 to 04:02. Notice the main advantage for range queries and the significant disadvantage related to hotspots.
As summarized in the video:
- Pro: Very efficient for range queries. If you want to fetch all orders from January 2024, the router knows to query only Shard 3. This is a huge performance win.
- Con: Highly susceptible to hotspots. If our partitioning key is a timestamp or an auto-incrementing ID, all new data will be written to the same shard. This last shard becomes a write bottleneck, while the others are mostly idle for writes.
3. Strategy 2: Hash-based Partitioning
To overcome the hotspot problem of range partitioning, we can use a hash function to distribute keys more uniformly across shards.
Part A: Simple Hash Partitioning
The most basic form is to hash the partitioning key and then apply a modulo operator with the number of shards.
Partition ID = hash(partitioning_key) % N
Where N is the number of shards.
A good hash function will map seemingly random keys, spreading the load evenly. For example, user_id_12345 and user_id_12346 are likely to end up on completely different shards, eliminating the sequential write hotspot we saw with range partitioning.
Trade-offs
Google SWE teaches systems design | EP5: Database sharding/partitioning
Now, let's examine the trade-offs of hash partitioning. It solves one problem but introduces another.
Watch from 04:02 to 05:33. Focus on why hash partitioning breaks range queries and the subtle issue that hotspots can still occur if a single key is extremely popular.
- Pro: Excellent for uniform data and load distribution, assuming a good hash function. This makes it ideal for workloads with many point lookups (e.g., fetching a user profile by
user_id). - Con: Destroys the natural ordering of keys. Running a range query (e.g., "find all users with IDs between 1000 and 2000") is now disastrously inefficient, as it would require querying every single shard.
- The Big Engineering Problem: What happens when you need to add or remove a shard? If you change
N(the number of shards), the result ofhash(key) % Nchanges for almost every key! This would require a massive, system-wide reshuffling of data, which is operationally infeasible for large systems.
Consistent Hashing | Algorithms You Should Know #1
The ByteByteGo channel has an excellent video that perfectly illustrates why the 'modulo N' approach is a non-starter in dynamic systems.
Watch from 00:20 to 02:32. This segment clearly shows the 'storm' of data movement that occurs when a server is added or removed using this simple hashing method.
Part B: Consistent Hashing — The Solution
To solve this massive re-shuffling problem, distributed systems use a more sophisticated technique called Consistent Hashing. This is a critical algorithm for any engineer working on scalable systems.
The core idea is to map both the nodes (shards) and the data keys onto the same abstract circle (a "hash ring"). To find the shard for a key, you hash the key to find its position on the ring and then travel clockwise until you find a node. That node is responsible for the key.
Lecture Notes - 23 Introduction to Distributed Databases
The CMU lecture notes provide a concise, academic explanation of Consistent Hashing.
Read the paragraph on 'Consistent Hashing' in the 'Handling Cluster Size Changes' section. It's located just below Figure 3. This will give you a formal definition to complement the videos.
Now, let's see how this works visually.
Consistent Hashing | Algorithms You Should Know #1
This next segment from the ByteByteGo video explains the hash ring mechanism and demonstrates why it's so effective.
Watch from 02:32 to 04:55. Observe how, when a node is added or removed, only a small fraction of keys (from the adjacent node) need to be moved. This is the key benefit.
Implementation Logic and "Virtual Nodes"
To implement consistent hashing, you need a data structure that can efficiently find the next node on the ring. A balanced binary search tree or a sorted array (with binary search) containing the hash values of the nodes would work well.
// Pseudocode for Consistent Hashing
class ConsistentHasher {
// A map of node hash values to node IDs, kept sorted by hash value.
private SortedMap<Integer, NodeID> ring;
// Add a node to the ring
public void addNode(NodeID nodeId) {
ring.put(hash(nodeId), nodeId);
}
// Get the node responsible for a key
public NodeID getNodeForKey(Key k) {
int keyHash = hash(k);
// Find all nodes with hashes greater than the key's hash
SortedMap<Integer, NodeID> tailMap = ring.tailMap(keyHash);
int nodeHash;
if (tailMap.isEmpty()) {
// Wrap around: key is after the last node, so it belongs to the first node
nodeHash = ring.firstKey();
} else {
// The first node found clockwise is the correct one
nodeHash = tailMap.firstKey();
}
return ring.get(nodeHash);
}
}
One final problem remains: if nodes are placed randomly on the ring, some might get a much larger share of the keys than others. The solution is to use virtual nodes. Instead of placing one point on the ring for each physical node, we place many (e.g., 100) virtual points for each node. This smooths out the distribution, making it statistically much more likely that each physical node gets a similar amount of data.
Consistent Hashing | Algorithms You Should Know #1
Let's finish the ByteByteGo video to see how virtual nodes solve the load-balancing problem.
Watch from 05:09 to 07:11. This is a crucial implementation detail that makes consistent hashing practical.
Consistent hashing, with virtual nodes, is the de-facto standard for hash partitioning in many popular NoSQL databases like Amazon DynamoDB and Apache Cassandra.
Test your understanding!
You are designing two new services:
- Analytics Service: Ingests event logs with timestamps. A common query is "show all events that occurred between time
T1andT2." - User Profile Service: Stores user data, which is almost always fetched by a unique
user_id. The service needs to scale horizontally as millions of new users sign up.
For each service, which partitioning strategy (Range or Hash) would you choose, and why? What specific challenges will you need to address for your chosen strategy?
Show answer
-
Analytics Service: The best choice is Range Partitioning with the
timestampas the partitioning key.- Why: The primary query pattern is a range scan. Range partitioning allows the system to direct these queries to a minimal set of shards, making them highly efficient.
- Challenge: This will create a write hotspot, as all new events (with current timestamps) will go to the last partition. This might be acceptable for an analytics system where write latency is less critical than read performance, or it could be mitigated by having smaller, time-bounded partitions (e.g., a new partition for every hour).
-
User Profile Service: The best choice is Hash Partitioning on the
user_id, implemented with Consistent Hashing.- Why: Queries are point lookups, so the loss of key order is not an issue. Hash partitioning will distribute the user profiles evenly across the shards, preventing hotspots and ensuring good load balance. Consistent hashing is critical to allow the system to add more nodes seamlessly as the user base grows, without requiring a massive data reshuffle.
- Challenge: The main implementation complexity is building the consistent hashing logic, including managing the ring and virtual nodes to ensure fair distribution. Range queries (e.g., "find all users in a certain region," if region is part of the ID) would become impractical.
Conclusion
In this lesson, we explored the two primary strategies for data partitioning in a shared-nothing system. The choice between them is a classic engineering trade-off driven by your application's query patterns.
Key Takeaways:
- Partitioning (Sharding) is the process of splitting table data horizontally across multiple nodes.
- Range Partitioning is simple and highly efficient for range queries but is vulnerable to creating hotspots with sequential keys.
- Hash Partitioning provides excellent load balancing for point lookups but makes range queries inefficient.
- Simple "modulo N" hashing is not viable in dynamic systems due to the massive data reshuffling required when nodes are added or removed.
- Consistent Hashing is a critical algorithm that solves this rebalancing problem, enabling hash-partitioned systems to scale elastically with minimal data movement.
Preview of the Next Lesson
Now that we understand how to distribute data across multiple nodes, the next question is: how do we execute a query that needs this distributed data? In our next lesson, we will explore techniques for parallel query execution, starting with how to explain inter-operator and intra-operator parallelism in a shared-nothing system.