Data Partitioning skill for Claude Code
Pick shard keys from data, not from hope. Claude only recommends sharding after indexes, replicas, caching and native partitioning are ruled out, then picks the key from measured query patterns and runs a bundled simulator on real key frequencies to show skew, hot keys and how much data moves when a shard is added. It plans cross-shard queries, unique IDs and a rehearsed slot-by-slot resharding procedure before launch.
- name
data-partitioning - size196 lines · ≈2,250 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Plans and reviews partitioning and sharding of data stores - when to partition within one database versus shard across nodes versus adopt distributed SQL, choosing partition and shard keys from real access patterns, hash, range, directory and logical-slot strategies, hot keys and celebrity tenants, cross-shard queries and transactions, globally unique IDs, online resharding with dual writes and backfill, Postgres declarative partitioning, and DynamoDB key design, with a key-skew simulator. Use when a table or database outgrows one node, picking a shard key, a shard runs hot, or planning a resharding migration. Try asking
- Our orders table is 4 TB and growing. Do we partition, shard, or something else?
- Is tenant_id a good shard key? Here are row counts per tenant
- Plan moving our largest customers onto dedicated shards without downtime
Use it when
- A table or database is outgrowing a single node
- Choosing a partition or shard key
- One shard or partition runs much hotter than the others
- Planning a resharding or tenant move
Skip it when
- The problem is a slow query that an index would fix — use sql-query-tuning
- You are designing tenant isolation rather than scale — use multi-tenancy
How to check the result
The skew simulation on real key weights passes the thresholds, most top queries include the key, and a staging rehearsal of a slot move matches row counts and checksums.
The common mistake it prevents
Sharding by a timestamp or another monotonic key, which sends every new write to the same shard while the others sit idle.
Done well means: sharding was chosen only after cheaper options ran out, the key was picked from measured access patterns and simulated for skew, almost every query carries the key, hot keys have a plan, IDs are unique without a central sequence, and resharding is a rehearsed procedure rather than an emergency.
Exhaust cheaper options first
| Pressure | Try before sharding |
|---|---|
| Slow queries | Indexes, query fixes, sql-query-tuning |
| Read load | Read replicas, caching, materialized views |
| Table too big for maintenance (vacuum, index builds) | Partitioning within one database |
| Old data dominates | Partition by time and archive or drop old partitions |
| Write throughput on one node | Bigger instance, batching, fewer indexes, async writes |
| Storage | Bigger volumes, compression, moving blobs to object storage |
Shard when a single primary cannot hold the write rate or working set after these, or when tenancy and data residency demand physical separation.
| Option | Use when |
|---|---|
Native partitioning (Postgres PARTITION BY) | One node is enough; you need pruning, faster maintenance, cheap retention |
| Application-level sharding | Clear tenant/user key, team can own routing and resharding |
| Citus, Vitess, PlanetScale | Want sharding with SQL and tooling instead of building routing |
| CockroachDB, Spanner, YugabyteDB, TiDB | Need automatic range splitting and distributed transactions; accept latency cost |
| DynamoDB, Cassandra, ScyllaDB | Key-value access patterns known up front, massive scale |
Workflow
- [ ] 1. Collect access patterns and size/growth numbers
- [ ] 2. List candidate keys; check each query includes it
- [ ] 3. Simulate skew with real key frequencies (scripts/shard_skew.py)
- [ ] 4. Choose the strategy (hash, range, directory, logical slots)
- [ ] 5. Plan hot keys, cross-shard queries, unique IDs, and transactions
- [ ] 6. Plan resharding before launch; rehearse it
- [ ] 7. Write the design and verification steps
1. Access patterns
For the top 10 to 20 queries by frequency and by cost, record: filter columns, sort, rows returned, read/write ratio, and latency target. Also: current size, growth per month, peak write rate, and the largest single tenant or user as a share of the total.
2. Key selection
A good shard key has:
- High cardinality (many more distinct values than shards; ideally 100x+).
- Even load: no value carries more than a fraction of one shard’s capacity.
- Query locality: 90%+ of queries include it, and related rows share it (a tenant’s
orders, items and invoices all keyed by
tenant_id). - Stability: the value never changes for a row (changing it means moving the row).
| Workload | Typical key | Watch out for |
|---|---|---|
| B2B SaaS | tenant_id | One huge tenant; plan dedicated shards |
| Consumer app | user_id | Social features that join across users |
| Events, logs, metrics | Time range (partition) + hash of source (shard) | Monotonic time keys make the newest partition hot |
| Marketplace orders | buyer_id or seller_id | Queries by the other side become scatter-gather; denormalize |
| IoT | device_id | Fleets with a few chatty devices |
3. Simulate
Export real key frequencies (rows, requests, or bytes per key) and run:
# key,weight per line, e.g. from: SELECT tenant_id, count(*) FROM orders GROUP BY 1
python3 scripts/shard_skew.py tenant_rows.csv --shards 8
python3 scripts/shard_skew.py tenant_rows.csv --shards 16 --slots 4096 --max-skew 1.2
It reports skew (max/mean shard load) for hash-mod-N, a consistent-hash ring, and logical slots, the share of data moved when adding one shard, and keys heavier than half a mean shard. Exit code 1 means the key fails the thresholds. Target skew at or below 1.2; a hot key flagged here cannot be fixed by any hash function.
4. Strategy
| Strategy | Pros | Cons | Use when |
|---|---|---|---|
| Hash mod N | Even spread, trivial | Changing N moves almost everything | Never for stores that will grow; fine for fixed partition counts |
| Logical slots (hash mod S, S fixed e.g. 1024 or 4096, slots mapped to shards) | Move whole slots to rebalance; mapping is a small table | Need the slot map in routing | Default for application sharding |
| Consistent hashing with virtual nodes | Adding a node moves about 1/N | Skew depends on vnode count | Caches, Dynamo-style stores |
| Range | Range scans, time-based retention | Hot newest range with monotonic keys | Time series partitions; ordered scans |
| Directory (lookup table key -> shard) | Place any tenant anywhere, move individually | Lookup on every request (cache it) | B2B with very uneven tenants, data residency |
A common combination: directory for large or regulated tenants, logical slots for everyone else.
5. Hot keys, cross-shard work, IDs
Hot keys:
- Celebrity tenant: give it a dedicated shard via the directory.
- Write-hot single key (a global counter, a viral post): split it into N sub-keys
(
post:123#0..15) and sum on read, or buffer writes and apply in batches. - Monotonic keys in range-partitioned stores: prefix with a hash bucket or use random UUIDs for the clustering key.
Cross-shard queries:
| Need | Approach |
|---|---|
| Admin or analytics across all data | Replicate to a warehouse (CDC); never scatter-gather in OLTP paths |
| Lookups by a secondary attribute (email -> user) | Global lookup table keyed by that attribute, or a search index |
| Lists spanning shards (“all orders for seller” when sharded by buyer) | Denormalized copy keyed the other way, maintained by events |
| Rare scatter-gather | Parallel fan-out with per-shard timeouts and limits; cap result size |
Transactions: keep each business transaction inside one shard by choosing a key that colocates its rows. When unavoidable, use a saga with compensations rather than two-phase commit across application shards.
IDs: no per-shard auto-increment exposed as global IDs. Use UUIDv7 (time-ordered, good index locality) or Snowflake-style IDs; optionally embed the slot in the ID so routing needs no lookup.
6. Resharding
Plan it before launch:
- Add the target shard(s); create schema.
- Mark moving slots or tenants as “migrating” in the routing table.
- Dual-write: writes go to old and new location (or capture changes via CDC from old).
- Backfill historical rows in batches, idempotently (upserts by primary key).
- Verify: row counts and checksums per slot match; compare a sample of reads.
- Cut over reads, then writes, slot by slot; keep the old copy read-only for a soak period.
- Delete old copies only after explicit approval.
Rehearse on staging with production-sized data and measure duration per slot. Moving whole logical slots (or whole tenants) keeps the procedure the same at every scale.
Postgres partitioning specifics
CREATE TABLE events (
id uuid NOT NULL,
tenant_id bigint NOT NULL,
created_at timestamptz NOT NULL,
payload jsonb NOT NULL,
PRIMARY KEY (id, created_at) -- unique constraints must include the partition key
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2026_10 PARTITION OF events
FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
CREATE TABLE events_default PARTITION OF events DEFAULT;
- Queries must filter on the partition key for pruning; check
EXPLAINshows only the expected partitions. - Create partitions ahead of time (pg_partman or a scheduled job); rows landing in the default partition block later attaching a partition for that range.
- Retention:
ALTER TABLE events DETACH PARTITION events_2025_01 CONCURRENTLY;then drop or archive it. Dropping is irreversible; confirm with the user. - Keep partition counts in the low hundreds to low thousands; planning time grows with partition count.
DynamoDB specifics
- Each partition serves up to about 3,000 read units and 1,000 write units per second; adaptive capacity helps but does not fix a single hot key.
- Design keys from access patterns (single-table design when patterns are known); add write sharding suffixes for hot partition keys.
- GSIs have their own partitions; a low-cardinality GSI key (status) creates hot spots.
Gotchas
- Choosing
created_atas the shard key: every write hits the newest shard. - Sharding by a key that some queries lack turns them into scatter-gather forever.
- Foreign keys and unique constraints do not span shards; enforce cross-shard uniqueness with a lookup table.
- Connection counts multiply: app instances x pool size x shards.
- Schema migrations must run on every shard; automate and track per-shard state.
Output contract
## Why partition / shard now
Measurements and options already exhausted
## Key and strategy
Key, strategy, simulation output (skew, moved share, hot keys)
## Query coverage
| Query | Includes key? | Plan if not |
## Hot keys, IDs, transactions
## Resharding procedure and rehearsal results
## Risks and rollback Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
scripts/shard_skew.py
#!/usr/bin/env python3
"""Simulate how a candidate shard key spreads load across shards.
Input: a CSV or text file of observed keys, one per line as `key` or `key,weight`
(weight = rows, requests, or bytes for that key; default 1). A header line starting
with "key" is skipped. Use "-" to read stdin.
Usage:
python3 scripts/shard_skew.py keys.csv --shards 8 [--vnodes 128] [--slots 1024]
[--max-skew 1.25] [--hot-key-share 0.5]
For each strategy (hash mod N, consistent-hash ring with virtual nodes, fixed logical
slots mapped to shards) it prints per-shard load, skew (max / mean), and the share of
load that moves when growing from N to N+1 shards. It also lists the hottest keys.
Exit codes: 0 = key distributes acceptably, 1 = skew or hot keys exceed thresholds,
2 = bad input.
"""
import argparse
import bisect
import hashlib
import statistics
import sys
from collections import Counter
def h64(text: str) -> int:
return int.from_bytes(hashlib.md5(text.encode("utf-8")).digest()[:8], "big")
def read_keys(path: str) -> Counter:
fh = sys.stdin if path == "-" else open(path, encoding="utf-8")
weights: Counter = Counter()
with fh:
for n, line in enumerate(fh, 1):
line = line.strip()
if not line or (n == 1 and line.lower().startswith("key")):
continue
key, _, w = line.partition(",")
try:
weights[key.strip()] += float(w) if w.strip() else 1.0
except ValueError:
sys.exit(f"error: line {n}: weight '{w}' is not a number")
if not weights:
print("error: no keys read", file=sys.stderr)
sys.exit(2)
return weights
class ModN:
name = "hash mod N"
def __init__(self, shards: int, **_):
self.n = shards
def shard(self, key: str) -> int:
return h64(key) % self.n
class Ring:
name = "consistent-hash ring"
def __init__(self, shards: int, vnodes: int = 128, **_):
points = sorted((h64(f"shard-{s}#vn-{v}"), s) for s in range(shards) for v in range(vnodes))
self.hashes = [p[0] for p in points]
self.owners = [p[1] for p in points]
def shard(self, key: str) -> int:
i = bisect.bisect(self.hashes, h64(key)) % len(self.hashes)
return self.owners[i]
class Slots:
name = "logical slots"
def __init__(self, shards: int, slots: int = 1024, assignment=None, **_):
self.slots = slots
self.assignment = assignment or [s % shards for s in range(slots)]
def shard(self, key: str) -> int:
return self.assignment[h64(key) % self.slots]
def grown(self, new_shards: int) -> "Slots":
"""Move the minimum number of slots to a new shard, taking evenly from the others."""
assignment = list(self.assignment)
target = self.slots // new_shards
owned = Counter(assignment)
new_id = new_shards - 1
moved = 0
for slot, owner in enumerate(assignment):
if moved >= target:
break
if owned[owner] > target:
assignment[slot] = new_id
owned[owner] -= 1
moved += 1
return Slots(new_shards, self.slots, assignment)
def loads(strategy, weights: Counter, shards: int):
per = [0.0] * shards
for key, w in weights.items():
per[strategy.shard(key)] += w
return per
def moved_share(old, new, weights: Counter) -> float:
total = sum(weights.values())
moved = sum(w for k, w in weights.items() if old.shard(k) != new.shard(k))
return moved / total
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("file")
ap.add_argument("--shards", type=int, required=True)
ap.add_argument("--vnodes", type=int, default=128)
ap.add_argument("--slots", type=int, default=1024)
ap.add_argument("--max-skew", type=float, default=1.25, help="max/mean shard load allowed (default 1.25)")
ap.add_argument("--hot-key-share", type=float, default=0.5,
help="flag keys heavier than this fraction of a mean shard (default 0.5)")
ap.add_argument("--top", type=int, default=5)
args = ap.parse_args()
if args.shards < 1 or args.slots < args.shards:
print("error: need shards >= 1 and slots >= shards", file=sys.stderr)
return 2
weights = read_keys(args.file)
total = sum(weights.values())
mean = total / args.shards
n = args.shards
print(f"{len(weights)} distinct keys, total weight {total:,.0f}, {n} shards, mean load {mean:,.1f}\n")
strategies = [
(ModN(n), ModN(n + 1)),
(Ring(n, vnodes=args.vnodes), Ring(n + 1, vnodes=args.vnodes)),
]
slots = Slots(n, slots=args.slots)
strategies.append((slots, slots.grown(n + 1)))
print(f"{'strategy':<22} {'skew max/mean':>13} {'stdev %':>8} {'moved on N+1':>13}")
results = {}
for cur, nxt in strategies:
per = loads(cur, weights, n)
skew = max(per) / mean if mean else 0
stdev = (statistics.pstdev(per) / mean * 100) if mean else 0
mv = moved_share(cur, nxt, weights)
results[cur.name] = (skew, per)
print(f"{cur.name:<22} {skew:>13.3f} {stdev:>7.1f}% {mv * 100:>12.1f}%")
best = min(results, key=lambda k: results[k][0])
print(f"\nper-shard load ({best}):")
for i, v in enumerate(results[best][1]):
bar = "#" * int(40 * v / max(results[best][1]))
print(f" shard {i:>3}: {v:>12,.1f} {bar}")
print(f"\ntop {args.top} keys:")
hot = []
for key, w in weights.most_common(args.top):
share_of_shard = w / mean if mean else 0
flag = " <-- hot key" if share_of_shard > args.hot_key_share else ""
if flag:
hot.append(key)
print(f" {key:<30} {w:>12,.1f} {w / total * 100:5.1f}% of total, {share_of_shard:5.2f}x mean shard{flag}")
problems = []
if results[best][0] > args.max_skew:
problems.append(f"best skew {results[best][0]:.3f} exceeds --max-skew {args.max_skew}")
if hot:
problems.append(f"{len(hot)} hot key(s) exceed {args.hot_key_share}x a mean shard; no hash spreads a single key")
print()
if problems:
for p in problems:
print(f"FAIL: {p}")
print("Consider a higher-cardinality key, a compound key, dedicated shards, or salting hot keys.")
return 1
print("OK: key distributes within thresholds.")
return 0
if __name__ == "__main__":
sys.exit(main())
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.