Estrategias de Particionamiento de Datos
A medida que los datos crecen más allá de lo que un solo servidor de base de datos puede manejar eficientemente, el particionamiento se vuelve esencial. El particionamiento de datos (o sharding) distribuye datos entre múltiples máquinas, habilitando escalado horizontal tanto para almacenamiento como para rendimiento de consultas. Elegir la estrategia de particionamiento correcta tiene implicaciones profundas para el rendimiento de consultas, distribución de datos y complejidad operacional.
Particionamiento Horizontal vs. Vertical
Estos son dos enfoques fundamentalmente diferentes para dividir datos.
| Aspecto | Particionamiento Horizontal (Sharding) | Particionamiento Vertical |
|---|---|---|
| Qué se divide | Las filas se distribuyen entre particiones | Las columnas se dividen en tablas/servicios separados |
| Esquema | Cada partición tiene el mismo esquema | Cada partición tiene un subconjunto de columnas |
| Caso de uso | Tablas grandes con miles de millones de filas | Tablas con muchas columnas, algunas raramente accedidas |
| Ejemplo | Usuarios A-M en shard 1, N-Z en shard 2 | Perfil de usuario en una tabla, preferencias en otra |
| Escalabilidad | Escala a muchos shards | Limitado por el número de grupos de columnas |
El particionamiento vertical suele ser el primer paso: separar columnas frecuentemente accedidas de las grandes o raramente usadas. El particionamiento horizontal se usa cuando el conteo de filas excede la capacidad de una sola máquina.
Criterios de Particionamiento
Para el particionamiento horizontal, la decisión clave es cómo asignar filas a particiones. Hay tres estrategias principales.
Particionamiento Basado en Clave (Hash)
Aplica una función hash a una clave de partición (ej. ID de usuario) y usa el resultado para determinar qué partición almacena los datos.
// Key-based partitioning
function getPartition(key: string, numPartitions: number): number {
const hash = hashFunction(key);
return hash % numPartitions;
}
// Problem: Adding a partition reshuffles almost all keys!
// Solution: Consistent hashing
class ConsistentHash {
private ring: Map<number, string> = new Map();
private sortedPositions: number[] = [];
private virtualNodes: number;
constructor(nodes: string[], virtualNodes: number = 150) {
this.virtualNodes = virtualNodes;
for (const node of nodes) {
this.addNode(node);
}
}
addNode(node: string): void {
for (let i = 0; i < this.virtualNodes; i++) {
const position = this.hash(`${node}:${i}`);
this.ring.set(position, node);
this.sortedPositions.push(position);
}
this.sortedPositions.sort((a, b) => a - b);
}
getNode(key: string): string {
const hash = this.hash(key);
const idx = this.sortedPositions.findIndex((p) => p >= hash);
const position = this.sortedPositions[idx >= 0 ? idx : 0];
return this.ring.get(position)!;
}
private hash(key: string): number {
let h = 0;
for (let i = 0; i < key.length; i++) {
h = (h * 31 + key.charCodeAt(i)) | 0;
}
return Math.abs(h);
}
}Particionamiento Basado en Rango
Asigna rangos continuos de la clave de partición a cada partición. Esto es ideal para datos de series temporales o cuando las consultas por rango son comunes.
// Range-based partitioning
interface RangePartition {
partitionId: string;
startKey: string;
endKey: string;
server: string;
}
const partitions: RangePartition[] = [
{ partitionId: "p1", startKey: "A", endKey: "H", server: "server1" },
{ partitionId: "p2", startKey: "H", endKey: "O", server: "server2" },
{ partitionId: "p3", startKey: "O", endKey: "Z", server: "server3" },
];
// For time-series: partition by date range
const timePartitions: RangePartition[] = [
{ partitionId: "2025-Q1", startKey: "2025-01-01", endKey: "2025-04-01", server: "server1" },
{ partitionId: "2025-Q2", startKey: "2025-04-01", endKey: "2025-07-01", server: "server2" },
{ partitionId: "2025-Q3", startKey: "2025-07-01", endKey: "2025-10-01", server: "server3" },
];Particionamiento Basado en Lista (Directorio)
Usa una tabla de búsqueda que mapea valores de clave específicos a particiones. Esto es útil cuando los datos tienen agrupaciones naturales como país o región.
const regionPartitionMap: Record<string, string> = {
"US": "partition-americas",
"CA": "partition-americas",
"BR": "partition-americas",
"UK": "partition-europe",
"DE": "partition-europe",
"FR": "partition-europe",
"JP": "partition-asia",
"KR": "partition-asia",
"IN": "partition-asia",
};
function getPartitionForUser(countryCode: string): string {
return regionPartitionMap[countryCode] || "partition-default";
}| Estrategia | Mejor Para | Cuidado Con |
|---|---|---|
| Basado en hash | Distribución uniforme, consultas puntuales | Consultas por rango requieren scatter-gather |
| Basado en rango | Consultas por rango, datos de series temporales | Puntos calientes si los datos no se distribuyen uniformemente |
| Basado en lista | Agrupaciones naturales, localidad de datos | Tamaños de partición desiguales, requiere gestión manual |
Rebalanceo de Particiones
A medida que los datos crecen o la carga cambia, las particiones necesitan rebalancearse. Esto involucra mover datos entre nodos sin tiempo de inactividad.
- Número fijo de particiones: Crear muchas más particiones que nodos (ej. 1000 particiones, 10 nodos). Asignar ~100 particiones por nodo. Al agregar un nodo, mover algunas particiones. Usado por Riak, Elasticsearch y Couchbase.
- Particionamiento dinámico: Dividir particiones cuando crecen demasiado, fusionar cuando son muy pequeñas. Usado por HBase y MongoDB.
- Particionamiento proporcional: Cada nodo mantiene un número fijo de particiones. Cuando un nuevo nodo se une, divide algunas particiones existentes y toma la mitad. Usado por Cassandra.
Regla de Seguridad de Rebalanceo
Nunca dejes que el rebalanceo ocurra automáticamente sin confirmación del operador en producción. El rebalanceo automático durante una partición de red o fallo de nodo puede escalar a una interrupción mayor. La mayoría de sistemas maduros requieren aprobación manual del operador para operaciones de rebalanceo.
Puntos Calientes y Mitigación
Los puntos calientes ocurren cuando una partición recibe desproporcionadamente más tráfico que otras. Las causas comunes incluyen usuarios celebridad, contenido viral y claves basadas en tiempo donde todas las escrituras van a la partición del tiempo actual.
// Hot spot mitigation: Add random suffix to hot keys
function mitigateHotKey(key: string, isHot: boolean): string {
if (!isHot) return key;
const suffix = Math.floor(Math.random() * 100);
return `${key}_${suffix}`;
}
// Reading requires scatter-gather across all suffixed keys
async function readHotKey(baseKey: string): Promise<number> {
const promises = Array.from({ length: 100 }, (_, i) =>
db.get(`${baseKey}_${i}`)
);
const values = await Promise.all(promises);
return values.reduce((sum, v) => sum + (v || 0), 0);
}Particionamiento en la Práctica
PostgreSQL (Particionamiento Declarativo)
// PostgreSQL supports RANGE, LIST, and HASH partitioning natively
// SQL:
// CREATE TABLE orders (
// id BIGSERIAL,
// created_at TIMESTAMP NOT NULL,
// customer_id INT,
// total DECIMAL
// ) PARTITION BY RANGE (created_at);
//
// CREATE TABLE orders_2025_q1 PARTITION OF orders
// FOR VALUES FROM ('2025-01-01') TO ('2025-04-01');
// PostgreSQL automatically routes queries to the correct partition
// SELECT * FROM orders WHERE created_at = '2025-03-15';
// Only scans orders_2025_q1MongoDB (Sharding)
// MongoDB sharding with hashed shard key
// db.adminCommand({
// shardCollection: "mydb.users",
// key: { userId: "hashed" }
// });
// Compound shard key for better distribution
// db.adminCommand({
// shardCollection: "mydb.messages",
// key: { channelId: 1, timestamp: 1 }
// });Cassandra (Partition Key + Clustering Key)
// Cassandra uses a partition key (hashed) + clustering key (sorted)
// CREATE TABLE messages (
// channel_id UUID, -- partition key: determines which node
// message_id TIMEUUID, -- clustering key: sorted within partition
// user_id UUID,
// content TEXT,
// PRIMARY KEY (channel_id, message_id)
// ) WITH CLUSTERING ORDER BY (message_id DESC);
// This design:
// - Distributes channels across nodes (hash of channel_id)
// - Stores messages within a channel sorted by time
// - Supports efficient range queries within a channelMejores Prácticas
- Elige la clave de partición cuidadosamente. Debe distribuir datos uniformemente y alinearse con tus patrones de consulta más comunes
- Evita consultas entre particiones. Diseña tu esquema para que las consultas comunes solo impacten una sola partición
- Planifica para el crecimiento. Usa hashing consistente o un número grande fijo de particiones para evitar rebalanceos dolorosos
- Monitorea tamaños de particiones. Alerta cuando cualquier partición es 3x más grande que el promedio
- Considera claves compuestas. Combinar una clave bien distribuida con una clave de rango te da tanto distribución uniforme como consultas de rango eficientes
- Comienza sin sharding. Una sola instancia PostgreSQL bien afinada puede manejar millones de filas. Solo haz sharding cuando tengas una necesidad clara