Llega un momento en la vida de cualquier aplicación de éxito en el que la base de datos se convierte en el cuello de botella. Las consultas que tardaban milisegundos empiezan a tardar segundos, los índices ya no caben cómodamente en memoria, los backups tardan horas y la ventana de mantenimiento desaparece porque el sistema nunca puede permitirse pararse. El particionado y el sharding son las dos grandes estrategias para escalar bases de datos más allá de los límites de una sola máquina o una sola tabla.
Sin embargo, ambas técnicas añaden complejidad real. Esta guía te ayuda a entender qué son, cuándo tiene sentido aplicarlas y cómo funcionan en la práctica.
La diferencia fundamental: particionado vs sharding
Aunque se usan a veces como sinónimos, son estrategias distintas con implicaciones diferentes:
Particionado: dividir una tabla grande en partes más pequeñas dentro del mismo servidor de base de datos. El motor gestiona la partición de forma transparente: desde la aplicación sigues escribiendo consultas normales contra la misma tabla, y el motor decide en qué partición buscar o escribir. Es una característica que ofrecen MySQL y PostgreSQL de forma nativa.
Sharding: distribuir los datos entre múltiples servidores de base de datos independientes. Cada servidor (shard) tiene un subconjunto de los datos. La aplicación (o una capa intermedia) necesita saber en qué shard está cada dato y dirigir las consultas al servidor correcto. Es una solución de escala horizontal que el motor de base de datos no gestiona por sí solo en la mayoría de casos.
Dicho de otra manera: el particionado es escalar verticalmente la organización de los datos dentro de un servidor; el sharding es escalar horizontalmente distribuyendo los datos entre varios servidores.
Cuándo considerar estas técnicas
Antes de añadir complejidad, vale la pena agotar las optimizaciones más simples. El orden correcto es:
- Índices correctos: la mayoría de problemas de rendimiento se resuelven aquí.
- Optimizar consultas: reescribir subconsultas como JOINs, evitar SELECT *, usar covering indexes.
- Caché: Redis o Memcached para reducir la carga de lecturas repetidas.
- Réplicas de lectura: añadir réplicas para distribuir las lecturas sin tocar la escritura.
- Hardware más potente: más RAM, SSD más rápido, más CPUs.
- Particionado: cuando la tabla es tan grande que los índices no caben en memoria o el mantenimiento (vacuum, backups, borrado de datos históricos) es prohibitivo.
- Sharding: cuando ningún servidor único puede manejar la carga de escritura, incluso con todo lo anterior aplicado.
El sharding en particular debe ser el último recurso: añade complejidad operacional significativa, hace que las transacciones distribuidas y los JOINs entre shards sean muy difíciles, y es muy costoso de revertir una vez implementado.
Particionado en PostgreSQL
PostgreSQL soporta particionado declarativo desde la versión 10. Defines la tabla padre con la estrategia de particionado y luego creas las tablas hijas para cada partición.
Particionado por rango (RANGE): el más habitual
Ideal para datos temporales: cada partición contiene un rango de fechas. Los datos históricos se pueden archivar o eliminar borrando simplemente una partición, que es instantáneo aunque la partición tenga millones de filas.
-- Crear la tabla particionada por rango de fechas
CREATE TABLE pedidos (
id BIGINT GENERATED ALWAYS AS IDENTITY,
cliente_id INT NOT NULL,
total DECIMAL(10, 2),
estado VARCHAR(20),
creado_en TIMESTAMP NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (creado_en);
-- Crear las particiones para cada trimestre de 2026
CREATE TABLE pedidos_2026_q1
PARTITION OF pedidos
FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
CREATE TABLE pedidos_2026_q2
PARTITION OF pedidos
FOR VALUES FROM ('2026-04-01') TO ('2026-07-01');
CREATE TABLE pedidos_2026_q3
PARTITION OF pedidos
FOR VALUES FROM ('2026-07-01') TO ('2026-10-01');
CREATE TABLE pedidos_2026_q4
PARTITION OF pedidos
FOR VALUES FROM ('2026-10-01') TO ('2027-01-01');
-- Crear índices: se aplican automáticamente a todas las particiones
CREATE INDEX idx_pedidos_cliente ON pedidos(cliente_id);
CREATE INDEX idx_pedidos_estado ON pedidos(estado, creado_en);
-- Insertar y consultar: igual que una tabla normal
INSERT INTO pedidos (cliente_id, total, estado)
VALUES (123, 59.99, 'completado');
SELECT * FROM pedidos WHERE creado_en >= '2026-04-01' AND creado_en < '2026-07-01';
-- PostgreSQL solo escanea pedidos_2026_q2 (partition pruning)
-- Ver qué partición se usará para una consulta (partition pruning)
EXPLAIN SELECT * FROM pedidos WHERE creado_en >= '2026-04-01';
-- Verás que solo aparecen las particiones relevantes, no todas
-- Añadir particiones para el año siguiente (sin afectar al sistema en producción)
CREATE TABLE pedidos_2027_q1
PARTITION OF pedidos
FOR VALUES FROM ('2027-01-01') TO ('2027-04-01');
-- Archivar datos antiguos: simplemente desconectar la partición
ALTER TABLE pedidos DETACH PARTITION pedidos_2026_q1;
-- pedidos_2026_q1 ahora es una tabla independiente (no se borra, se archiva)
-- O eliminar datos históricos de golpe (instantáneo aunque tenga millones de filas)
DROP TABLE pedidos_2026_q1; -- equivale a borrar millones de filas pero en milisegundos
Particionado por lista (LIST)
Útil cuando quieres separar los datos por categorías discretas: país, región, tipo de usuario, estado del pedido.
-- Tabla particionada por país
CREATE TABLE usuarios (
id BIGINT GENERATED ALWAYS AS IDENTITY,
nombre VARCHAR(100),
email VARCHAR(100),
pais VARCHAR(2) NOT NULL, -- código ISO: 'ES', 'MX', 'AR', etc.
activo BOOLEAN DEFAULT TRUE
) PARTITION BY LIST (pais);
CREATE TABLE usuarios_europa
PARTITION OF usuarios
FOR VALUES IN ('ES', 'PT', 'FR', 'DE', 'IT', 'GB', 'NL');
CREATE TABLE usuarios_latam
PARTITION OF usuarios
FOR VALUES IN ('MX', 'CO', 'AR', 'CL', 'PE', 'VE', 'EC');
CREATE TABLE usuarios_otros
PARTITION OF usuarios
DEFAULT; -- captura todos los países no listados explícitamente
-- Las consultas filtradas por país solo escanean la partición correcta
SELECT COUNT(*) FROM usuarios WHERE pais = 'ES';
-- Solo escanea usuarios_europa
Particionado por hash (HASH)
Distribuye las filas de forma uniforme entre N particiones usando un hash de la columna elegida. Es la opción cuando no hay una columna de rango o lista natural, pero se quiere distribuir la carga uniformemente.
-- Particionado por hash: distribuir uniformemente entre 8 particiones
CREATE TABLE eventos (
id BIGINT GENERATED ALWAYS AS IDENTITY,
usuario_id INT NOT NULL,
tipo VARCHAR(50),
datos JSONB,
timestamp TIMESTAMP DEFAULT NOW()
) PARTITION BY HASH (usuario_id);
-- Crear 8 particiones (módulo 8, resto 0 a 7)
CREATE TABLE eventos_p0 PARTITION OF eventos FOR VALUES WITH (MODULUS 8, REMAINDER 0);
CREATE TABLE eventos_p1 PARTITION OF eventos FOR VALUES WITH (MODULUS 8, REMAINDER 1);
CREATE TABLE eventos_p2 PARTITION OF eventos FOR VALUES WITH (MODULUS 8, REMAINDER 2);
CREATE TABLE eventos_p3 PARTITION OF eventos FOR VALUES WITH (MODULUS 8, REMAINDER 3);
CREATE TABLE eventos_p4 PARTITION OF eventos FOR VALUES WITH (MODULUS 8, REMAINDER 4);
CREATE TABLE eventos_p5 PARTITION OF eventos FOR VALUES WITH (MODULUS 8, REMAINDER 5);
CREATE TABLE eventos_p6 PARTITION OF eventos FOR VALUES WITH (MODULUS 8, REMAINDER 6);
CREATE TABLE eventos_p7 PARTITION OF eventos FOR VALUES WITH (MODULUS 8, REMAINDER 7);
Particionado en MySQL
-- Particionado por rango de año en MySQL
CREATE TABLE pedidos (
id INT AUTO_INCREMENT,
cliente_id INT NOT NULL,
total DECIMAL(10, 2),
fecha DATE NOT NULL,
PRIMARY KEY (id, fecha) -- la columna de partición DEBE estar en la clave primaria
)
PARTITION BY RANGE (YEAR(fecha)) (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION p2026 VALUES LESS THAN (2027),
PARTITION p_futuro VALUES LESS THAN MAXVALUE -- captura fechas futuras
);
-- Verificar que se usa partition pruning
EXPLAIN SELECT * FROM pedidos WHERE fecha >= '2026-01-01';
-- Eliminar una partición (archivado de datos históricos)
ALTER TABLE pedidos DROP PARTITION p2023;
-- Añadir una partición nueva
ALTER TABLE pedidos REORGANIZE PARTITION p_futuro INTO (
PARTITION p2027 VALUES LESS THAN (2028),
PARTITION p_futuro VALUES LESS THAN MAXVALUE
);
-- Particionado por lista en MySQL
CREATE TABLE pedidos_por_estado (
id INT AUTO_INCREMENT,
estado VARCHAR(20) NOT NULL,
total DECIMAL(10,2),
fecha DATE,
PRIMARY KEY (id, estado)
)
PARTITION BY LIST COLUMNS(estado) (
PARTITION p_activos VALUES IN ('pendiente', 'procesando', 'enviado'),
PARTITION p_cerrados VALUES IN ('completado', 'cancelado', 'devuelto')
);
-- Ver las particiones de una tabla
SELECT PARTITION_NAME, TABLE_ROWS, DATA_LENGTH
FROM information_schema.PARTITIONS
WHERE TABLE_NAME = 'pedidos';
Sharding: distribuir entre múltiples servidores
Estrategias de sharding
La decisión más importante del sharding es la shard key: la columna por la que se decide en qué servidor vive cada dato. Una buena shard key distribuye los datos uniformemente y permite que la mayoría de consultas vayan a un solo shard.
Sharding por rango: los datos se distribuyen según rangos del valor de la shard key. Shard 1 tiene IDs del 1 al 10 millones, Shard 2 del 10M al 20M, etc. Fácil de implementar y de entender, pero puede crear hot spots si la actividad se concentra en un rango (por ejemplo, siempre se insertan IDs nuevos en el último shard).
Sharding por hash: se aplica una función hash a la shard key y el resultado módulo N determina el shard. Distribuye uniformemente los datos y elimina los hot spots, pero hace casi imposibles las consultas por rango entre shards.
Sharding por directorio: una tabla de mapeo externa determina dónde está cada dato. Más flexible pero introduce una dependencia en la tabla de mapeo que se convierte en un punto único de fallo.
-- Ejemplo conceptual de lógica de sharding por hash en la aplicación
SHARD_COUNT = 4
def obtener_shard(usuario_id):
"""Determina en qué shard está el usuario dado su ID"""
return usuario_id % SHARD_COUNT
def obtener_conexion(usuario_id):
"""Devuelve la conexión al shard correcto"""
shard_num = obtener_shard(usuario_id)
conexiones = {
0: "postgresql://usuario@shard-0.ejemplo.com/db",
1: "postgresql://usuario@shard-1.ejemplo.com/db",
2: "postgresql://usuario@shard-2.ejemplo.com/db",
3: "postgresql://usuario@shard-3.ejemplo.com/db"
}
return conectar(conexiones[shard_num])
# Para el usuario 12345:
# 12345 % 4 = 1 → va al shard 1
# La consulta se dirige al servidor shard-1.ejemplo.com
def buscar_pedidos_usuario(usuario_id):
conn = obtener_conexion(usuario_id)
return conn.execute(
"SELECT * FROM pedidos WHERE usuario_id = %s",
[usuario_id]
)
El problema de las consultas cruzadas entre shards
-- Esto es trivial en una base de datos sin sharding:
SELECT u.nombre, COUNT(p.id) AS total_pedidos
FROM usuarios u
JOIN pedidos p ON p.usuario_id = u.id
WHERE u.pais = 'ES'
GROUP BY u.id, u.nombre;
-- Con sharding, los usuarios están distribuidos entre 4 servidores
-- y los pedidos también están distribuidos (idealmente en el mismo shard que el usuario)
-- Pero si necesitas datos de varios shards a la vez, tienes que:
-- 1. Hacer la consulta en cada shard por separado
-- 2. Combinar los resultados en la capa de aplicación
def total_pedidos_por_pais(pais):
resultados = []
for shard_num in range(SHARD_COUNT):
conn = conectar_shard(shard_num)
filas = conn.execute("""
SELECT u.nombre, COUNT(p.id) AS total_pedidos
FROM usuarios u
JOIN pedidos p ON p.usuario_id = u.id
WHERE u.pais = %s
GROUP BY u.id, u.nombre
""", [pais])
resultados.extend(filas)
# Combinar y ordenar en Python
return sorted(resultados, key=lambda x: x['total_pedidos'], reverse=True)
# Este scatter-gather es mucho más lento y complejo que una sola consulta
Herramientas de sharding
Implementar el sharding directamente en la aplicación es posible pero complejo. Hay herramientas que lo gestionan de forma más transparente:
Citus es una extensión de PostgreSQL que convierte un clúster de PostgreSQL en una base de datos distribuida con sharding automático. Las consultas SQL normales funcionan (con algunas limitaciones) y Citus se encarga de distribuirlas entre los nodos.
-- Con Citus instalado, el sharding se configura de forma declarativa
-- en lugar de hacerlo en la aplicación
-- Convertir una tabla normal en una tabla distribuida
SELECT create_distributed_table('pedidos', 'usuario_id');
-- Citus crea automáticamente los shards y distribuye los datos
-- A partir de aquí, las consultas normales funcionan y Citus las distribuye
SELECT COUNT(*) FROM pedidos WHERE usuario_id = 12345;
-- Citus dirige la consulta al nodo correcto automáticamente
-- Para consultas que necesitan varios shards, Citus las paraleliza
SELECT pais, COUNT(*) FROM usuarios GROUP BY pais;
-- Citus ejecuta la consulta en todos los nodos y combina los resultados
Vitess es la solución de sharding para MySQL que usa YouTube y muchos otros sistemas a gran escala. Actúa como un proxy entre la aplicación y los servidores MySQL, gestionando el routing de consultas, el resharding y la administración del clúster.
PlanetScale (basado en Vitess) y Neon son servicios gestionados que ofrecen sharding y escalado sin que tengas que operar la infraestructura tú mismo.
El problema del resharding
Uno de los mayores dolores de cabeza del sharding es el resharding: si empiezas con 4 shards y luego necesitas 8, tienes que mover la mitad de los datos de cada shard existente a nuevos shards. Este proceso es complejo, caro y arriesgado sin las herramientas correctas.
-- Estrategia de consistent hashing para reducir el impacto del resharding
-- En lugar de usar usuario_id % N (que requiere mover todos los datos al cambiar N),
-- se usa un anillo de hash donde añadir un shard solo mueve 1/N de los datos
-- Concepto simplificado:
# Con hash simple (% 4):
# Si pasamos de 4 a 5 shards, el 80% de los datos cambia de shard
# Con consistent hashing:
# Si pasamos de 4 a 5 shards, solo el 20% de los datos cambia de shard
# porque cada nuevo shard solo "toma" datos de sus vecinos en el anillo
-- En la práctica, Citus maneja el resharding de forma más controlada:
-- SELECT rebalance_table_shards('pedidos');
-- Redistribuye los shards de forma incremental sin downtime
Réplicas de lectura: el paso anterior al sharding
Antes de llegar al sharding, muchos sistemas escalan las lecturas con réplicas. Una réplica es una copia de la base de datos principal que recibe todos los cambios del primario en tiempo real y puede servir consultas de lectura. Si el 80% de tus consultas son de lectura (lo más habitual), añadir réplicas puede multiplicar la capacidad de lectura sin la complejidad del sharding.
-- Lógica de routing read/write split en la aplicación
class ConexionBaseDatos:
def __init__(self):
# Un servidor primario para escrituras
self.primario = conectar("postgresql://primario.ejemplo.com/db")
# Varias réplicas para lecturas
self.replicas = [
conectar("postgresql://replica-1.ejemplo.com/db"),
conectar("postgresql://replica-2.ejemplo.com/db"),
conectar("postgresql://replica-3.ejemplo.com/db")
]
self._replica_actual = 0
def escritura(self):
"""Todas las escrituras van al primario"""
return self.primario
def lectura(self):
"""Las lecturas se distribuyen entre réplicas (round-robin)"""
replica = self.replicas[self._replica_actual % len(self.replicas)]
self._replica_actual += 1
return replica
db = ConexionBaseDatos()
# Las escrituras van al primario
db.escritura().execute("INSERT INTO pedidos ...")
# Las lecturas van a las réplicas
db.lectura().execute("SELECT * FROM productos WHERE categoria = 'Electrónica'")
Cuándo usar cada estrategia: guía de decisión
| Situación | Estrategia recomendada |
|---|---|
| Tabla con millones de filas y consultas lentas a pesar de índices correctos | Particionado |
| Necesitas borrar datos históricos rápidamente | Particionado por rango con DROP PARTITION |
| Muchas lecturas, pocas escrituras | Réplicas de lectura |
| La carga de escritura supera lo que puede hacer un solo servidor | Sharding |
| Necesitas escalar PostgreSQL con mínimo cambio en la aplicación | Citus |
| Necesitas escalar MySQL a escala de YouTube/Facebook | Vitess o PlanetScale |
| Startup que está creciendo rápido y no quiere gestionar infraestructura | Bases de datos gestionadas (PlanetScale, Neon, Supabase) que escalan por ti |
Resumen
- El particionado divide una tabla grande en particiones dentro del mismo servidor. El motor gestiona la distribución de forma transparente: la aplicación no necesita cambiar. MySQL y PostgreSQL lo soportan de forma nativa.
- El sharding distribuye los datos entre múltiples servidores independientes. La aplicación (o una capa proxy como Citus o Vitess) necesita gestionar el routing de consultas al shard correcto.
- Antes de llegar a estas técnicas, asegúrate de haber agotado índices correctos, optimización de consultas, caché y réplicas de lectura. El sharding en particular es una inversión muy costosa de hacer y de revertir.
- En PostgreSQL, el particionado declarativo (
PARTITION BY RANGE/LIST/HASH) es la opción más sencilla y potente para tablas muy grandes, especialmente para datos temporales. - La shard key es la decisión más importante del sharding: una mala elección crea hot spots y hace las consultas cruzadas imposibles.
- El resharding (cambiar el número de shards) es el mayor dolor de cabeza operacional: planifica con margen usando consistent hashing o herramientas como Citus que lo gestionan automáticamente.
No hay comentarios todavía. Sé el primero en compartir tu opinión.