Database Sharding
database sharding. Cubre range-based, hash-based y directory-based partitioning strategies, consistent hashing, shard key selection, cross-shard queries, resharding, Vitess, Citus y cuando shardar vs escalar verticalmente con ejemplos practicos.
Introducción
Sharding splitea un database en smaller pieces (shards) distributed across multiple servers. Cada shard holda un subset del data. A continuacion se cubre partitioning strategies, shard key selection, consistent hashing, cross-shard queries, resharding, y tools como Vitess y Citus.
When to Shard
Shardea cuando:
- Data no fittea en una single machine (disk o memory)
- Write throughput excede una single server's capacity
- Query latency aumenta a medida que data grows
- Necesitas geographic data distribution
NO shardea cuando:
- Data fittea en una single machine con headroom
- Podes scalear verticalmente (bigger server, mas RAM, SSD)
- Application logic no puede handle cross-shard complexity
- Necesitas ACID transactions across shards
Sharding adda:
- Operational complexity (mas servers para manage)
- Application complexity (routing queries a shards)
- Cross-shard query limitations
- Resharding difficulty cuando el shard key cambia
Rule: shardea last, despues de vertical scaling, read replicas, y caching.
Partitioning Strategies
Range-Based Sharding
Range-based sharding assigna data a shards basado en value ranges del shard key.
Shard 1: user_id 1 - 1,000,000
Shard 2: user_id 1,000,001 - 2,000,000
Shard 3: user_id 2,000,001 - 3,000,000
def get_shard(user_id: int) -> int:
if user_id <= 1_000_000:
return 1
elif user_id <= 2_000_000:
return 2
else:
return 3
# Pros: range queries son efficient (scan un shard)
# Cons: hot spots — recent users van al last shard
# uneven distribution si keys no son uniform
Hash-Based Sharding
Hash-based sharding aplica un hash function al shard key para determinar el shard.
import hashlib
def get_shard(user_id: str, num_shards: int = 4) -> int:
hash_value = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
return hash_value % num_shards
# Pros: even distribution, no hot spots
# Cons: range queries requieren scanning all shards
# resharding es expensive (all data debe ser redistributed)
# Example distribution
users = ["alice", "bob", "charlie", "diana", "eve", "frank"]
for user in users:
print(f"{user}: shard {get_shard(user)}")
# alice: shard 2
# bob: shard 0
# charlie: shard 3
# diana: shard 1
# eve: shard 2
# frank: shard 0
Directory-Based Sharding
Un lookup table mapea shard keys a shards. Un dedicated service maneja el mapping.
import redis
# Shard directory stored en Redis
r = redis.Redis(host="localhost", port=6379)
# Initialize shard mapping
shard_mapping = {
"us-east": "shard1.example.com",
"us-west": "shard2.example.com",
"eu-central": "shard3.example.com",
"asia-pacific": "shard4.example.com",
}
for region, host in shard_mapping.items():
r.hset("shard_directory", region, host)
def get_shard_connection(region: str) -> str:
host = r.hget("shard_directory", region)
if not host:
raise ValueError(f"Unknown region: {region}")
return host.decode()
# Pros: flexible — podes move data entre shards sin cambiar el hash function
# Cons: lookup adda latency, directory es un single point of failure
# requiere un highly available directory service
Consistent Hashing
Consistent hashing minimiza data movement cuando adding o removing shards.
import hashlib
import bisect
class ConsistentHashRing:
def __init__(self, virtual_nodes: int = 150):
self.virtual_nodes = virtual_nodes
self.ring: list[tuple[int, str]] = []
self.sorted_keys: list[int] = []
def _hash(self, key: str) -> int:
return int(hashlib.md5(key.encode()).hexdigest(), 16)
def add_node(self, node: str) -> None:
for i in range(self.virtual_nodes):
hash_val = self._hash(f"{node}:{i}")
bisect.insort(self.sorted_keys, hash_val)
self.ring.insert(
bisect.bisect_left(self.sorted_keys, hash_val),
(hash_val, node)
)
def remove_node(self, node: str) -> None:
self.ring = [(h, n) for h, n in self.ring if n != node]
self.sorted_keys = [h for h, n in self.ring]
def get_node(self, key: str) -> str:
if not self.sorted_keys:
raise ValueError("No nodes in ring")
hash_val = self._hash(key)
idx = bisect.bisect_right(self.sorted_keys, hash_val)
if idx == len(self.sorted_keys):
idx = 0
return self.ring[idx][1]
# Usage
ring = ConsistentHashRing(virtual_nodes=150)
ring.add_node("shard1.example.com")
ring.add_node("shard2.example.com")
ring.add_node("shard3.example.com")
print(ring.get_node("user:12345")) # shard2.example.com
print(ring.get_node("user:67890")) # shard1.example.com
# Addear un node solo mueve un fraction de keys
ring.add_node("shard4.example.com")
# Solo ~25% de keys mueven al new node
Consistent hashing benefits:
- Addear un node: solo K/N keys mueven (K = total keys, N = nodes)
- Remove un node: solo K/N keys mueven
- Virtual nodes mejoran distribution uniformity
- No need de rehashear all keys cuando topology cambia
Used by: Cassandra, DynamoDB, Redis Cluster, Memcached clients
Shard Key Selection
Good shard key properties:
- High cardinality — many distinct values para even distribution
- Low frequency — ningun single value domina (avoids hot spots)
- Non-monotonic — no siempre increase (avoids all new data en un shard)
- Query-relevant — most queries incluyen el shard key (avoids scatter)
Bad shard keys:
- Auto-increment ID — monotonic, all new data va al last shard
- Timestamp — recent data concentrated en un shard
- Low cardinality field (e.g., country con 3 values) — uneven distribution
- Field no en queries — every query scannea all shards
Good shard keys:
- User ID (UUID) — high cardinality, included en most queries
- Hash de (user_id + timestamp) — non-monotonic, high cardinality
- Composite key (user_id, created_at) — soporta range queries per user
# Example: elegir un shard key para un multi-tenant app
# Bad: shard por tenant_id — un large tenant overwhelma un shard
# Good: shard por (tenant_id, user_id) — distribute dentro de un tenant
def get_shard_key(tenant_id: str, user_id: str) -> str:
return f"{tenant_id}:{user_id}"
# Most queries incluyen tenant_id y user_id
# SELECT * FROM orders WHERE tenant_id = 'acme' AND user_id = 'u123'
# Routea a un single shard
Cross-Shard Queries
-- Single-shard query (efficient — incluye shard key)
SELECT * FROM orders WHERE user_id = 123 AND created_at > '2026-01-01';
-- Cross-shard query (scatter-gather — query all shards)
SELECT * FROM orders WHERE status = 'pending';
-- Router manda query a all shards, mergea results
-- Cross-shard aggregation (expensive)
SELECT COUNT(*) FROM orders WHERE created_at > '2026-01-01';
-- Cada shard countea, router summa los counts
-- Cross-shard join (very expensive — avoid)
SELECT u.name, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 'shipped';
-- Requiree fetchar data desde multiple shards y joinear in memory
# Scatter-gather pattern en application code
import concurrent.futures
def scatter_gather(query: str, shards: list[str]) -> list[dict]:
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=len(shards)) as executor:
futures = {
executor.submit(execute_on_shard, shard, query): shard
for shard in shards
}
for future in concurrent.futures.as_completed(futures):
shard = futures[future]
try:
shard_results = future.result()
results.extend(shard_results)
except Exception as e:
print(f"Error on {shard}: {e}")
return results
def execute_on_shard(shard: str, query: str) -> list[dict]:
# Connect a shard y execute query
conn = connect_to_shard(shard)
return conn.execute(query).fetchall()
Resharding
Cuando reshard:
- Data grows beyond current shard capacity
- Hot shard (uneven distribution)
- Adding o removing shards
Resharding strategies:
1. Dual-write: write a both old y new shards, backfill, luego switch reads
2. Capture-change: stream changes desde old shards a new, luego switch
3. Offline: stop writes, migrate data, restart (downtime)
Dual-write process:
1. Add new shards alongside old ones
2. Write a both old y new (dual-write)
3. Backfill existing data desde old a new
4. Verify data consistency
5. Switch reads a new shards
6. Stop writes a old shards
7. Decommission old shards
# Dual-write example
class DualWriteRouter:
def __init__(self, old_shards, new_shards):
self.old_shards = old_shards
self.new_shards = new_shards
self.read_from = "old" # Switch a "new" despues de verification
def write(self, shard_key: str, data: dict):
# Write a both old y new
old_shard = self.get_shard(shard_key, self.old_shards)
new_shard = self.get_shard(shard_key, self.new_shards)
old_shard.insert(data)
new_shard.insert(data)
def read(self, shard_key: str, query: str):
if self.read_from == "old":
return self.get_shard(shard_key, self.old_shards).query(query)
else:
return self.get_shard(shard_key, self.new_shards).query(query)
def get_shard(self, key: str, shards: list) -> object:
idx = hash(key) % len(shards)
return shards[idx]
Vitess
Vitess es un database clustering system para horizontal scaling de MySQL.
# vttablet configuration
tablet:
keyspace: commerce
shard: 0
tablet_alias: zone1-0000000100
db:
host: localhost
port: 3306
user: vt_app
password: vt_password
dbname: vt_commerce
-- Vitess usa VSchema para cross-shard queries
-- VSchema define como tables son sharded
-- Sharded table (by user_id)
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
total DECIMAL(10,2),
created_at TIMESTAMP
);
-- VSchema: orders es sharded by user_id (vindex)
-- "hash" vindex usa consistent hashing
-- Unsharded table (lookup table)
CREATE TABLE products (
id BIGINT PRIMARY KEY,
name VARCHAR(255),
price DECIMAL(10,2)
);
-- VSchema: products es unsharded (en un single shard)
-- Cross-shard query (Vitess handlea scatter-gather)
SELECT * FROM orders WHERE total > 100;
-- Vitess routea a all shards y mergea results
Citus
Citus es un PostgreSQL extension que distribute data across multiple nodes.
-- Install Citus extension
CREATE EXTENSION citus;
-- Crear distributed table (sharded by user_id)
SELECT create_distributed_table('orders', 'user_id');
-- Citus automaticamente shardea el table
-- Default: 32 shards, hash-based distribution
-- Query con shard key (routea a single shard)
SELECT * FROM orders WHERE user_id = 123;
-- Cross-shard query (scatter-gather)
SELECT count(*), status FROM orders GROUP BY status;
-- Reference table (replicated a all nodes)
SELECT create_reference_table('products');
-- Join distributed table con reference table
SELECT o.id, p.name, o.total
FROM orders o
JOIN products p ON o.product_id = p.id
WHERE o.user_id = 123;
-- Reference table es local en cada node, join es efficient
-- Colocated tables (same shard key, same shards)
SELECT create_distributed_table('order_items', 'user_id');
-- orders y order_items son colocated by user_id
-- Joins en user_id son single-shard y efficient
SELECT o.id, oi.product_id, oi.quantity
FROM orders o
JOIN order_items oi ON o.id = oi.order_id AND o.user_id = oi.user_id
WHERE o.user_id = 123;
See Also
Preguntas frecuentes
¿Cuál es la diferencia entre sharding y partitioning?
Partitioning divide un table dentro de un single database en smaller pieces. Puede ser horizontal (row-based) o vertical (column-based). Sharding distribute data across multiple database servers. Sharding es horizontal partitioning across machines. Partitioning stays en un server — mejora query performance y manageability. Sharding adda network communication y distributed query complexity. Usa partitioning first; shardea solo cuando un server no puede handle la data.
¿Cómo elijo un shard key?
Elegi un shard key con high cardinality (many distinct values), even distribution (no hot spots), y que aparezca en most queries (avoids scatter-gather). Evita monotonic keys como auto-increment IDs o timestamps — concentran new data en un shard. Evita low-cardinality fields como status o country. Good choices: UUIDs, composite keys como (tenant_id, user_id), o hashes de natural keys. Testea con real data distribution antes de commiting — cambiar el shard key later requiere resharding.
¿Qué es consistent hashing y por que importa?
Consistent hashing mapea tanto data keys como server nodes al same hash ring. Cuando un node es added o removed, solo las keys en ese node's portion del ring mueven. Esto minimiza data redistribution — tipicamente K/N keys mueven donde K es total keys y N es node count. Sin consistent hashing, addear un node con modulo hashing requiere redistributing all keys. Consistent hashing es used por Cassandra, DynamoDB, Redis Cluster, y most distributed caches.
¿Puedo hacer ACID transactions across shards?
Most sharded databases no soportan cross-shard ACID transactions. Cada shard es independent — un transaction en un shard no puede lockar rows en otro. Workarounds: usa two-phase commit (slow y complex), saga pattern (compensating transactions), o diseniá tu schema para que transactions stayan dentro de un single shard. Si tu application require cross-shard transactions, considerá si sharding es el right choice — un single server con vertical scaling puede ser better.
¿Cuándo deberia usar Vitess o Citus?
Usa Vitess cuando tenes un MySQL-based application que necesita horizontal scaling. Vitess provee connection pooling, query routing, y online schema migrations. Es used por YouTube, Slack, y GitHub. Usa Citus cuando tenes un PostgreSQL application que necesita horizontal scaling. Citus extiende PostgreSQL con distributed tables, reference tables, y colocated joins. Es un PostgreSQL extension, asi que mantenes full SQL compatibility. Ambas tools handlean sharding transparently — tu application ve un single database.
¿Cómo handleo joins across shards?
Cross-shard joins son expensive — el router debe fetchar data desde multiple shards y joinear in memory. Evitalos por: (1) colocando tables con el same shard key en los same shards, (2) usando reference tables (replicated a all shards), (3) denormalizando data para avoid joins, o (4) haciendo joins en application code despues de fetch desde shards. Citus handlea colocated joins eficientemente — si dos tables share el same shard key, joins en ese key son single-shard. Vitess soporta VSchema para similar optimization.
Recursos Relacionados
PostgreSQL Replication
PostgreSQL replication. Cubre streaming replication, logical replication, cascading replicas, synchronous commit, failover con Patroni, monitoring lag, slot management y disaster recovery con ejemplos practicos de configuracion.
GuideMongoDB Indexing
MongoDB indexing. Cubre single field, compound, text, geospatial, TTL, wildcard, hashed indexes, ESR rule, covered queries, explain plan analysis, index intersection y partial indexes con ejemplos practicos.
GuideSQL Query Optimization
Optimizar SQL queries. Cubre EXPLAIN plan analysis, index strategies, join optimization, N+1 query detection, query rewriting, materialized views, partitioning, connection pooling y query caching con ejemplos practicos de PostgreSQL y MySQL.
GuideElasticsearch Cluster Setup
Despliega y escala clusters de Elasticsearch. Cubre roles de nodos, sharding, réplicas, index templates, mapping, snapshots y tuning de producción para search a escala.
GuideBases de Datos Serverless
Elegir y operar bases de datos serverless para aplicaciones event-driven. Cubre DynamoDB, Aurora Serverless, FaunaDB y PlanetScale con pricing, escalado, patrones de query y estrategias de migracion.
GuideRedis en Producción
Correr Redis en produccion. Cubre persistence (RDB, AOF), clustering, sentinel para HA, failover handling, memory management, eviction policies, pipelining, Lua scripting, monitoring, security hardening y backup strategies con ejemplos practicos.