Database Sharding: Strategies and Patterns for Scalable Systems
Learn database sharding strategies and patterns to scale your database horizontally.
20+ years shipping production systems from the metal up. Drawn from code that ran under real load.
- ✓Understanding of basic database concepts (tables, queries, indexes).
- ✓Familiarity with SQL (CREATE, SELECT, INSERT).
- ✓Basic knowledge of distributed systems (CAP theorem, consistency models).
- Sharding splits a large database into smaller, independent partitions called shards.
- Each shard holds a subset of data and runs on a separate server.
- Common strategies: range-based, hash-based, directory-based, and geographic sharding.
- Sharding improves scalability and performance but adds complexity in queries and maintenance.
- Choose a sharding key carefully to avoid hotspots and ensure even data distribution.
Imagine a library with millions of books. Instead of one librarian managing all books (which would be slow), you split the books into separate rooms by genre. Each room has its own librarian. When you need a book, you go to the correct room. This is sharding: dividing data across multiple servers to handle more load and speed up access.
As your application grows, a single database server eventually becomes a bottleneck. Queries slow down, writes become contentious, and storage limits loom. Database sharding offers a path to horizontal scalability by partitioning your data across multiple independent databases (shards). Each shard operates as its own database, handling a subset of the data. This tutorial dives into the core sharding strategies—range-based, hash-based, directory-based, and geographic—with practical SQL examples and real-world trade-offs. You'll learn how to choose a sharding key, handle cross-shard queries, and avoid common pitfalls. By the end, you'll be equipped to design a sharded system that scales gracefully under load.
What is Database Sharding?
Database sharding is a horizontal partitioning technique that splits a large database into smaller, independent databases called shards. Each shard holds a subset of the data and runs on a separate server instance. The goal is to distribute load and overcome the limitations of a single server. Sharding is often used in large-scale applications like social networks, e-commerce platforms, and SaaS products. Unlike vertical scaling (adding more power to a single server), sharding scales out by adding more servers. This approach improves write throughput, read performance, and storage capacity. However, it introduces complexity in query routing, data consistency, and maintenance. The key to successful sharding is choosing an appropriate sharding key—the column used to determine which shard a row belongs to. A poor key can lead to uneven data distribution, hotspots, and performance bottlenecks.
Range-Based Sharding
Range-based sharding divides data based on a range of values of the sharding key. For example, users with IDs 1-10000 go to shard 1, 10001-20000 to shard 2, and so on. This strategy is simple to implement and allows efficient range queries within a shard. However, it can lead to uneven data distribution if the key values are not uniformly distributed. For instance, if new users are assigned increasing IDs, the latest shard may receive all writes, becoming a hotspot. Range sharding also makes rebalancing difficult because splitting a range requires moving large amounts of data. It is best suited for data that is naturally ordered and where access patterns are predictable, such as time-series data partitioned by date.
Hash-Based Sharding
Hash-based sharding applies a hash function to the sharding key to determine the shard. This ensures a uniform distribution of data across shards, reducing the risk of hotspots. Common hash functions include MD5, SHA-1, or a simple modulo operation. For example, shard = hash(user_id) % number_of_shards. This strategy works well for write-heavy workloads because writes are spread evenly. However, it makes range queries inefficient because data is scattered across shards. Also, adding or removing shards requires rehashing and moving data (re-sharding). Consistent hashing can mitigate this by minimizing data movement. Hash-based sharding is widely used in systems like Cassandra and DynamoDB.
Directory-Based Sharding
Directory-based sharding uses a lookup table (directory) to map each shard key to a specific shard. This offers flexibility: you can change the mapping without moving data, and you can assign different shards to different keys based on custom logic. For example, you might map high-value customers to dedicated shards. The downside is that the directory becomes a single point of failure and a performance bottleneck if not cached. It also adds latency for each query (one extra lookup). Directory-based sharding is useful when data distribution is uneven or when you need to support dynamic shard allocation. It is often used in combination with other strategies.
Geographic Sharding
Geographic sharding distributes data based on the geographic location of users or data. For example, users in North America go to a shard in US-East, European users to a shard in EU-West, etc. This reduces latency by placing data closer to users and can help comply with data residency regulations (e.g., GDPR). Geographic sharding is often combined with other strategies (e.g., hash within a region). The main challenge is handling users who move or travel, and ensuring consistent data across regions if needed. This strategy is common in global applications like social media and streaming services.
Choosing a Sharding Key
The sharding key is the most critical design decision. It determines how data is distributed and how queries are routed. A good sharding key should: (1) evenly distribute data to avoid hotspots, (2) support common query patterns (ideally, most queries target a single shard), (3) be stable (rarely changes), and (4) allow for future growth. Common choices include user ID, customer ID, or a hash of a natural key. Avoid using columns with low cardinality (e.g., gender) or that are frequently updated. Test your key choice with realistic data volumes. Also consider the trade-off between even distribution and query locality. For example, sharding by user ID spreads writes evenly but may scatter a user's related data across shards if not careful. Denormalization can help keep related data together.
Handling Cross-Shard Queries
Cross-shard queries (queries that need data from multiple shards) are expensive and should be minimized. They require scatter-gather: send the query to all shards, collect results, and combine. This increases latency and load. Strategies to reduce cross-shard queries include: (1) denormalization to keep related data in the same shard, (2) using a separate read-only replica for aggregations, (3) implementing application-level joins, and (4) using a distributed query engine (e.g., Presto, Spark). For transactions that span shards, consider using two-phase commit or eventual consistency with compensation logic. In many cases, it's better to design your data model to avoid cross-shard operations altogether.
Rebalancing and Resharding
Over time, data distribution may become uneven due to growth or changing access patterns. Rebalancing (moving data between shards) or resharding (changing the number of shards) may be necessary. This is a complex operation that must be done carefully to avoid downtime. Strategies include: (1) using consistent hashing to minimize data movement, (2) performing rebalancing in the background with throttling, (3) using a double-write pattern during migration (write to both old and new shards), and (4) using a proxy layer that handles routing changes. Tools like Vitess, Citus, and MongoDB's sharding features provide automated rebalancing. Plan for rebalancing from the start by designing your system to handle dynamic shard membership.
Distributed SQL: Automatic Sharding in CockroachDB, Spanner, Yugabyte
Distributed SQL databases like CockroachDB, Google Spanner, and YugabyteDB offer automatic sharding, abstracting the complexity of manual shard management. These systems use a consistent hash ring or range-based partitioning to distribute data across nodes, with automatic rebalancing and fault tolerance. For example, in CockroachDB, data is split into ranges (default ~64MB) and distributed across nodes. When a range grows, it automatically splits; when nodes are added or removed, ranges are rebalanced without downtime. This is achieved through a consensus protocol (Raft) ensuring strong consistency. Below is an example of creating a table in CockroachDB that is automatically sharded by the primary key:
``sql CREATE TABLE users ( user_id UUID PRIMARY KEY DEFAULT ``gen_random_uuid(), name STRING, email STRING UNIQUE, created_at TIMESTAMP );
CockroachDB automatically shards the users table by user_id. Queries are routed to the appropriate shard based on the key. For range scans, the system may fan out to multiple shards. Spanner uses a similar approach with interleaved tables and global indexes. YugabyteDB offers both hash and range sharding with automatic splitting. These databases handle cross-shard transactions using distributed transactions (e.g., Percolator in Spanner). The key benefit is that developers write standard SQL without manual sharding logic, though careful schema design (e.g., choosing a good primary key) is still important to avoid hot spots.
SHOW RANGES to inspect distribution. Avoid monotonically increasing keys (e.g., auto-increment) to prevent write hotspots.Application-Level Sharding vs Database-Level Sharding
Sharding can be implemented at the application level or the database level. Application-level sharding means the application code determines which database shard to use, often via a sharding library or custom logic. For example, the application might hash a user ID to select a shard and then connect to the appropriate database. This approach gives full control but requires the application to manage connections, handle cross-shard queries, and rebalance data. Below is a Python example using a simple hash-based shard router:
```python import hashlib
def get_shard(user_id, num_shards=4): hash_val = int(hashlib.md5(user_id.encode()).hexdigest(), 16) return hash_val % num_shards
# Usage shard_id = get_shard("user123") db = connect_to_shard(shard_id) ```
Database-level sharding is handled by the database system itself, such as in distributed SQL databases or middleware like Vitess. The application sends queries to a single endpoint, and the database routes them to the correct shard. This reduces application complexity but may limit flexibility. For example, in Vitess, you define a VSchema that maps tables to shards, and Vitess handles routing and aggregation. The choice depends on your team's expertise and requirements: application-level offers more control but higher maintenance, while database-level simplifies development but may have vendor lock-in. Hybrid approaches also exist, such as using a proxy layer like ProxySQL or MaxScale.
Resharding and Rebalancing: Strategies for Zero Downtime
Resharding (changing the number of shards or sharding key) and rebalancing (redistributing data across shards) are critical for scaling. Zero-downtime strategies include logical sharding, consistent hashing, and using a proxy layer. One common approach is to use a two-phase process: first, add new shards and start replicating data from old shards to new ones (dual writes). Then, once caught up, switch reads to the new shards. For example, with a consistent hash ring, you can add virtual nodes and gradually migrate data. Below is a simplified SQL example for a migration table that tracks rebalancing:
``sql CREATE TABLE shard_migration ( id SERIAL PRIMARY KEY, old_shard INT, new_shard INT, key_range_start INT, key_range_end INT, status VARCHAR(20) DEFAULT 'pending' ); ``
Tools like Vitess use a 'MoveTables' workflow to reshard with minimal downtime. Another strategy is to use a shard proxy that supports online schema changes and data migration. For range-based sharding, you can split a hot shard by dividing its key range. For hash-based sharding, you can increase the number of shards by rehashing (e.g., from 4 to 8 shards) using a double-write pattern during migration. The key is to ensure data consistency and minimal impact on users. Techniques like read-only mode for old shards during final cutover can help. Always test the migration process in a staging environment.
The Hot Shard That Brought Down a Social Media Feed
- Always monitor shard load distribution; hotspots can appear unexpectedly.
- Consider read-heavy workloads separately from write-heavy workloads when designing sharding keys.
- Use caching to reduce read pressure on hot shards.
- Plan for capacity scaling per shard, not just overall.
- Implement automatic rebalancing to handle skewed data distribution.
SELECT shard_id, COUNT(*) FROM request_log GROUP BY shard_id;SHOW PROCESSLIST;| File | Command / Code | Purpose |
|---|---|---|
| shard_creation.sql | CREATE DATABASE shard_1; | What is Database Sharding? |
| range_sharding.sql | SELECT CASE | Range-Based Sharding |
| hash_sharding.sql | SELECT user_id % 4 AS shard_id; | Hash-Based Sharding |
| directory_sharding.sql | CREATE TABLE shard_directory ( | Directory-Based Sharding |
| geo_sharding.sql | SELECT CASE | Geographic Sharding |
| sharding_key_example.sql | SELECT * FROM shard_3.orders WHERE customer_id = 123; | Choosing a Sharding Key |
| cross_shard_query.sql | SELECT * FROM shard_1.orders WHERE customer_id IN (1,2,3); | Handling Cross-Shard Queries |
| rebalancing.sql | CREATE DATABASE shard_5; | Rebalancing and Resharding |
| cockroachdb_auto_sharding.sql | CREATE TABLE users ( | Distributed SQL |
| app_level_sharding.py | def get_shard(user_id, num_shards=4): | Application-Level Sharding vs Database-Level Sharding |
| resharding_migration.sql | CREATE TABLE shard_migration ( | Resharding and Rebalancing |
Key takeaways
Common mistakes to avoid
4 patternsChoosing a sharding key that causes hotspots
Ignoring cross-shard query performance
Not planning for rebalancing
Using sharding as a silver bullet
Interview Questions on This Topic
Explain the difference between range-based and hash-based sharding. When would you use each?
Frequently Asked Questions
20+ years shipping production systems from the metal up. Drawn from code that ran under real load.
That's . Mark it forged?
6 min read · try the examples if you haven't