Skip to content
StackPractices
advanced Por Mathias Paulenko

Referencia Detallada de 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.

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

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;

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.

See Also

Guide

Complete Guide to PostgreSQL Replication

Master PostgreSQL replication. Covers streaming replication, logical replication, cascading replicas, synchronous commit, failover with Patroni, monitoring lag, slot management, and disaster recovery with practical configuration examples.

Guide

Complete Guide to MongoDB Indexing

Master MongoDB indexing. Covers single field, compound, text, geospatial, TTL, wildcard, hashed indexes, ESR rule, covered queries, explain plan analysis, index intersection, and partial indexes with practical examples.

Guide

Complete Guide to SQL Query Optimization

Optimize SQL queries. Covers EXPLAIN plan analysis, index strategies, join optimization, N+1 query detection, query rewriting, materialized views, partitioning, connection pooling, and query caching with practical PostgreSQL and MySQL examples.

Guide

Complete Guide to Elasticsearch Cluster Setup

Deploy and scale Elasticsearch clusters. Covers node roles, sharding, replicas, index templates, mapping, snapshots, and production tuning for search at scale.

Guide

Complete Guide to Serverless Databases

Choose and operate serverless databases for event-driven applications. Covers DynamoDB, Aurora Serverless, FaunaDB, and PlanetScale with pricing, scaling, query patterns, and migration strategies.

Guide

Complete Guide to Redis in Production

Run Redis in production. Covers persistence (RDB, AOF), clustering, sentinel for HA, failover handling, memory management, eviction policies, pipelining, Lua scripting, monitoring, security hardening, and backup strategies with practical examples.