Loading content...
If sharding is the foundation of database scaling, the shard key is the cornerstone of that foundation. Every other sharding decision—strategy (range, hash, directory), number of shards, rebalancing approach—flows from this single choice.
The shard key determines:
Choose well, and your system scales gracefully for years. Choose poorly, and you'll either suffer constant performance issues or face an expensive re-sharding migration.
By the end of this page, you will understand the principles of shard key selection, recognize common patterns and anti-patterns, analyze access patterns to identify optimal keys, and apply a decision framework to choose shard keys for real-world scenarios. This is perhaps the most important content in this entire module.
A shard key is the column (or combination of columns) used to determine which shard stores each row. Every row's shard is determined by applying the sharding function to its shard key value.
What Makes a Good Shard Key:
The ideal shard key satisfies multiple, sometimes conflicting, properties. Balancing these tradeoffs is the art of shard key selection.
1. High Cardinality
The shard key should have many distinct values to allow even distribution. A boolean column (true/false) is terrible—data splits into at most 2 shards. A UUID or auto-increment ID has essentially unlimited cardinality.
2. Even Distribution
Distinct values should occur with relatively equal frequency. A country column might have 200 values, but if 60% of users are in the US, you have severe skew.
3. Query Alignment
The shard key should appear in the WHERE clause of your most common queries. If 80% of queries filter by user_id, shard by user_id. If queries filter by created_at, consider time-based sharding.
4. Stability
The shard key value for a given entity should rarely change. If users frequently update their region, sharding by region means constantly moving data between shards.
| Property | What to Look For | Warning Signs | Impact if Missing |
|---|---|---|---|
| Cardinality | Millions+ of distinct values | < 100 distinct values | Cannot scale beyond N shards where N = cardinality |
| Distribution | Zipf < 1.5, no single key dominates | One value has >5% of data | Hot shards, wasted capacity |
| Query Alignment | Key in 80%+ of WHERE clauses | Most queries filter by other columns | Scatter-gather for common queries |
| Stability | Value set once, rarely changes | Frequent updates to key column | Data migration churn |
Treat the shard key as immutable after initial assignment. While you can technically update a shard key value, doing so requires moving the entire entity to a new shard—an expensive, error-prone operation. Design your data model so shard key values are permanent.
Certain shard key patterns appear repeatedly across different domains because they align with common access patterns. Understanding these patterns helps you recognize which applies to your situation.
Pattern 1: Tenant/Organization ID
For multi-tenant SaaS, shard by tenant_id (or org_id, account_id, workspace_id):
Examples: Salesforce, Slack, Shopify, Atlassian
Pattern 2: User ID
For user-centric applications, shard by user_id:
Examples: Facebook, Twitter, LinkedIn
Pattern 3: Timestamp
For time-series and logging systems, shard by time:
Examples: DataDog, Splunk, TimescaleDB
Pattern 4: Geographic Region
For geo-distributed applications, shard by region:
Examples: Uber, Doordash (for ride/delivery data)
Ask: 'What entity defines the boundary of most operations?' If the answer is 'tenant' (in SaaS), shard by tenant. If 'user' (in social), shard by user. If 'time period' (in analytics), shard by time. The shard key should match the natural isolation boundary of your domain.
Learning from failures is as important as learning from successes. These anti-patterns have caused production outages, expensive re-sharding projects, and abandoned systems.
status (active/inactive), country (200 values), or type (10 values) limits shard count forever.last_login or mutable status requires moving data whenever the value changes.email but you shard by user_id, you need email→user_id lookup before every query.category_id where millions of items share the same category creates massive skew.(tenant_id, user_id) but queries only filter by user_id require scatter-gather.Case Study: Sharding by Status
A team sharded an orders table by order_status:
Problems:
Result: Complete re-sharding to customer_id after 6 months.
Case Study: Sharding by Email Domain
A team sharded users by email domain for 'fraud detection':
Problems:
Result: Catastrophic failure at scale.
Even with good shard key choices (user_id), distribution can become skewed. One viral user (celebrity) can generate 1000x the data of normal users, overwhelming their shard. Solutions include: dedicated shards for celebrities, sub-sharding by (user_id, bucket), or rate limiting per-user data volume.
Sometimes a single column doesn't meet all shard key requirements. Composite shard keys combine multiple columns, providing finer-grained distribution and enabling efficient queries that filter by multiple dimensions.
When to Use Composite Keys:
Single column has low cardinality but combination is high
region (10 values) + user_id (millions) = high cardinalityQueries filter by multiple dimensions
tenant_id AND dateNeed hierarchical organization
org_id / team_id / user_id hierarchyCassandra's Compound Key Model:
Cassandra provides an elegant compound key model that many systems emulate:
PRIMARY KEY ((partition_key), clustering_columns)
Example: PRIMARY KEY ((tenant_id), created_at, event_id)
tenant_idcreated_at, then event_idcreated_at are efficient within a tenant1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
/** * Composite Shard Key Implementation * * Uses multiple columns to determine shard placement */ interface CompositeKey { primary: string; // Required, determines shard secondary?: string; // Optional, refines distribution} class CompositeShardRouter { private shardCount: number; private useSecondary: boolean; constructor(shardCount: number, useSecondary: boolean = false) { this.shardCount = shardCount; this.useSecondary = useSecondary; } /** * Compute shard from composite key */ getShard(key: CompositeKey): number { let hashInput: string; if (this.useSecondary && key.secondary) { // Hash both components hashInput = `${key.primary}:${key.secondary}`; } else { // Hash only primary hashInput = key.primary; } return this.hash(hashInput) % this.shardCount; } /** * Check if a query can be routed to single shard */ canRouteSingleShard(query: { primary?: string; secondary?: string }): boolean { // Must have primary key if (!query.primary) return false; // If using secondary for sharding, must have secondary if (this.useSecondary && !query.secondary) return false; return true; } private hash(input: string): number { let hash = 0; for (let i = 0; i < input.length; i++) { hash = Math.imul(31, hash) + input.charCodeAt(i) | 0; } return Math.abs(hash); }} // Example: IoT sensor data// Composite key: (device_id, date) const router = new CompositeShardRouter(16, true); // Query for specific device on specific date -> single shardconst shard1 = router.getShard({ primary: "device-abc123", secondary: "2024-01-15" });console.log(`device-abc123 on 2024-01-15: Shard ${shard1}`); // Same device, different date -> potentially different shardconst shard2 = router.getShard({ primary: "device-abc123", secondary: "2024-01-16" });console.log(`device-abc123 on 2024-01-16: Shard ${shard2}`); // Query routing analysisconsole.log("Can route single shard:");console.log(" device + date:", router.canRouteSingleShard({ primary: "d1", secondary: "2024-01-15" })); // trueconsole.log(" device only:", router.canRouteSingleShard({ primary: "d1" })); // false (scatter-gather)console.log(" date only:", router.canRouteSingleShard({ secondary: "2024-01-15" })); // false (scatter-gather)With composite keys, queries providing only part of the key require scatter-gather. If your composite key is (tenant_id, user_id), querying just by user_id requires checking all shards. Design your composite key so the most common query filter is the leading component.
The best shard key emerges from analyzing how your application actually accesses data. This is not a theoretical exercise—it requires examining real (or projected) query patterns.
Access Pattern Analysis Framework:
Step 1: Enumerate All Query Types
List every query your application runs. For each, note:
Step 2: Identify Filter Columns
For each query, which columns appear in WHERE clauses? Tally the frequency:
| Column | Queries Using It | Total QPS | Critical Path |
|---|---|---|---|
| user_id | 15/20 queries | 10,000 | Yes |
| tenant_id | 18/20 queries | 12,000 | Yes |
| created_at | 8/20 queries | 3,000 | No |
| status | 5/20 queries | 500 | No |
Step 3: Score Candidate Shard Keys
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
/** * Shard Key Candidate Scoring * * Analyzes access patterns to recommend optimal shard key */ interface Query { name: string; qps: number; // Queries per second filters: string[]; // Columns in WHERE clause isCriticalPath: boolean; // Part of user-facing latency isWrite: boolean; // Write operation} interface ShardKeyScore { column: string; singleShardQps: number; // QPS that hits single shard scatterGatherQps: number; // QPS that needs all shards criticalPathAlignment: number; // % of critical queries aligned writeAlignment: number; // % of writes aligned overallScore: number;} function analyzeShardKeyCandidates( queries: Query[], candidates: string[]): ShardKeyScore[] { return candidates.map(column => { let singleShardQps = 0; let scatterGatherQps = 0; let criticalAligned = 0; let criticalTotal = 0; let writeAligned = 0; let writeTotal = 0; for (const query of queries) { const aligned = query.filters.includes(column); if (aligned) { singleShardQps += query.qps; } else { scatterGatherQps += query.qps; } if (query.isCriticalPath) { criticalTotal += query.qps; if (aligned) criticalAligned += query.qps; } if (query.isWrite) { writeTotal += query.qps; if (aligned) writeAligned += query.qps; } } const totalQps = singleShardQps + scatterGatherQps; // Score: weighted combination of factors const overallScore = (singleShardQps / totalQps) * 40 + // 40% weight on single-shard ratio (criticalTotal > 0 ? criticalAligned / criticalTotal : 0) * 35 + // 35% on critical path (writeTotal > 0 ? writeAligned / writeTotal : 0) * 25; // 25% on write alignment return { column, singleShardQps, scatterGatherQps, criticalPathAlignment: criticalTotal > 0 ? criticalAligned / criticalTotal : 0, writeAlignment: writeTotal > 0 ? writeAligned / writeTotal : 0, overallScore, }; });} // Example: E-commerce application queriesconst queries: Query[] = [ { name: "Get user profile", qps: 5000, filters: ["user_id"], isCriticalPath: true, isWrite: false }, { name: "Get user orders", qps: 3000, filters: ["user_id"], isCriticalPath: true, isWrite: false }, { name: "Create order", qps: 500, filters: ["user_id"], isCriticalPath: true, isWrite: true }, { name: "Get order by ID", qps: 1000, filters: ["order_id"], isCriticalPath: true, isWrite: false }, { name: "Search products", qps: 2000, filters: ["category_id"], isCriticalPath: true, isWrite: false }, { name: "Admin: orders by status", qps: 10, filters: ["status"], isCriticalPath: false, isWrite: false }, { name: "Analytics: daily orders", qps: 5, filters: ["created_at"], isCriticalPath: false, isWrite: false },]; const candidates = ["user_id", "order_id", "category_id", "status"];const scores = analyzeShardKeyCandidates(queries, candidates); console.log("Shard Key Candidate Analysis:");console.log("============================");scores.sort((a, b) => b.overallScore - a.overallScore).forEach(score => { console.log(`${score.column}: Single-shard QPS: ${score.singleShardQps} Scatter-gather QPS: ${score.scatterGatherQps} Critical path alignment: ${(score.criticalPathAlignment * 100).toFixed(1)}% Write alignment: ${(score.writeAlignment * 100).toFixed(1)}% Overall Score: ${score.overallScore.toFixed(1)}`);}); // Expected output: user_id scores highestThe best input for access pattern analysis is production query logs. Use slow query logs, application APM data, or database query statistics. Real data beats assumptions every time. If building a new system, analyze the access patterns of similar systems or create realistic projections based on user flows.
No matter how well you choose your shard key, some operations will inevitably cross shard boundaries. The key is minimizing their frequency and handling them gracefully when they occur.
Types of Cross-Shard Operations:
Strategies for Each:
| Operation Type | Strategy | Tradeoff | Example |
|---|---|---|---|
| Scatter-gather query | Parallel query all shards, merge results | Latency = slowest shard | Search across all users |
| Cross-shard join | Query each side, join in application | Memory + network overhead | User → Orders (different shards) |
| Distributed transaction | 2PC or Saga pattern | Performance + complexity | Transfer between accounts |
| Global aggregation | Pre-compute in background job | Staleness | Total user count |
| Global secondary index | Maintain separate index service | Write overhead | Email → User lookup |
The Global Secondary Index Pattern:
For lookups by non-shard-key columns, maintain a separate index that maps alternative keys to shard keys:
Email Index: (email → user_id)
Username Index: (username → user_id)
The index can be:
Login Flow Example:
email_index[email] → user_idSELECT * FROM users WHERE id = user_idTwo lookups, but both are single-shard.
Enforcing global uniqueness (unique email across all shards) without a global index requires querying all shards on every insert—a scalability nightmare. Instead, use a global index for uniqueness enforcement: attempt to insert into the index first, fail if exists, then insert into the shard.
Let's integrate everything into a practical decision framework. Follow these steps to choose your shard key.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
# Shard Key Decision Document ## System: [Your System Name]## Table: [Table Being Sharded]## Date: [Decision Date] ## 1. Candidate Shard Keys | Candidate | Cardinality | Distribution | In Common Queries | Stability ||-----------|-------------|--------------|-------------------|-----------|| user_id | 100M+ | Even | 85% of queries | Immutable || tenant_id | 10K | Skewed (10%) | 90% of queries | Immutable || created_at| Continuous | Even | 30% of queries | Immutable | ## 2. Access Pattern Analysis Top 5 queries by QPS:1. Get user profile (user_id) - 5000 QPS, critical path2. Get user orders (user_id) - 3000 QPS, critical path3. Search products (category_id) - 2000 QPS, critical path4. Create order (user_id) - 500 QPS, critical path, write5. Get order by ID (order_id) - 1000 QPS, critical path ## 3. Shard Key Scores | Candidate | Single-Shard % | Critical Path % | Write % | Score ||-----------|----------------|-----------------|---------|-------|| user_id | 75% | 85% | 100% | 82 || tenant_id | 80% | 90% | 100% | 87 || order_id | 10% | 10% | 0% | 8 | ## 4. Selected Shard Key: tenant_id ### Rationale:- Highest critical path alignment (90%)- All writes routed to single shard- Multi-tenant isolation is business requirement- Large tenants can get dedicated shards ### Accepted Tradeoffs:- Product search requires scatter-gather (acceptable: uses Elasticsearch)- Cross-tenant analytics require scatter-gather (acceptable: batch job) ### Cross-Shard Operations:- Global user lookup: Email index service- Analytics: Nightly aggregation job- Admin dashboards: Scatter-gather (low volume) ## 5. Implementation Notes- Use hash(tenant_id) % 64 for initial sharding- Directory override for enterprise tenants- Review distribution quarterly ## 6. Approved By: [Names]A formal decision document prevents future debates, helps onboard new team members, and preserves the reasoning when you need to evaluate changes. When someone asks 'Why did we shard by tenant_id?', point them to the document.
We've covered the critical topic of shard key selection. Let's consolidate the key insights:
Module Complete:
Congratulations! You've completed the Sharding (Partitioning) module. You now understand:
With this knowledge, you can design and implement sharded database architectures that scale to millions of users and petabytes of data.
You now possess comprehensive knowledge of database sharding—from fundamental concepts to advanced implementation patterns. Apply this knowledge thoughtfully: shard when necessary, choose your shard key deliberately, and always design for the operations that matter most to your users.