
Particionamiento en PostgreSQL: guía para tablas de 200M+ filas
Cómo el particionamiento por rango en PostgreSQL redujo una consulta de 4.200ms a 18ms en una tabla de 200M de filas, sin añadir hardware. Guía práctica con ejemplos SQL.
Añadir RAM o vCPU a una base de datos cuando una tabla alcanza cientos de millones de registros no resuelve el problema de fondo. El particionamiento divide físicamente los datos por un criterio (como el tiempo) para que el engine solo lea las subtablas necesarias, reduciendo la I/O en un orden de magnitud.
El síntoma: la degradación silenciosa del p99
Cuando una tabla de persistencia crece sin una estrategia de particionamiento, el comportamiento es predecible: consultas que solían resolver en 15-30 ms comienzan a degradarse hasta alcanzar 2-3 segundos, sin que se haya desplegado código nuevo.
El primer instinto de los equipos es escalar verticalmente (vertical scaling): duplicar la RAM, subir los vCPUs o migrar a discos con más IOPS. Esto alivia el problema temporalmente porque el índice cabe en la buffer pool, pero termina colapsando. El problema no es de recursos, sino de diseño.
Por qué el hardware no resuelve el problema
A medida que la tabla alcanza cientos de millones de registros, la profundidad del árbol del índice B-Tree aumenta y su tamaño total supera la memoria RAM disponible.
Cuando el índice ya no cabe en la buffer pool:
- Cada consulta fuerza lecturas aleatorias en disco (page reads) para recorrer el árbol.
- Los fragmentos del índice desplazan a los datos calientes de la memoria.
- Se genera contención de locks en el buffer y un cuello de botella en I/O.
Doblar la RAM solo desplaza la fecha del colapso unas semanas. No cambia la complejidad del problema: la profundidad del índice (O(log N)) sigue creciendo con 200M de filas igual que con 2M — el hardware no altera esa relación.
Particionamiento en la práctica
El particionamiento (declarative partitioning) divide físicamente una tabla en subtablas independientes (particiones) basándose en una clave, habitualmente una columna temporal (fecha) o un rango de IDs.
Cuando el engine ejecuta una consulta que incluye la clave de partición en la cláusula WHERE, aplica Partition Pruning: descarta por completo las particiones que no corresponden al rango solicitado antes de tocar el disco.
-- Tabla padre particionada por rango temporal en PostgreSQL
CREATE TABLE transacciones (
id BIGINT NOT NULL,
fecha DATE NOT NULL,
cliente_id UUID NOT NULL,
monto NUMERIC(12,2) NOT NULL
) PARTITION BY RANGE (fecha);
-- Particiones físicas mensuales
CREATE TABLE transacciones_2026_01 PARTITION OF transacciones
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE transacciones_2026_02 PARTITION OF transacciones
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
Al consultar el periodo de febrero:
SELECT cliente_id, SUM(monto)
FROM transacciones
WHERE fecha >= '2026-02-01' AND fecha < '2026-03-01'
GROUP BY cliente_id;
El planificador de consultas (Query Planner) ignora la partición de enero y cualquier otra existente. La superficie de lectura en disco disminuye drásticamente.
Caso real: de 4.2s a 18ms
En una auditoría para un sistema transaccional con más de 180 millones de eventos auditables:
- Situación inicial: tabla de 140 GB. La consulta diaria de conciliación tardaba 4.200 ms, generando timeouts en el API Gateway y saturando los IOPS de la instancia PostgreSQL.
- Intervención: aplicamos particionamiento mensual por rango temporal y reestructuramos el índice compuesto para incluir la clave de partición (
fecha). - Resultados:
- Latencia p99: reducción de 4.200 ms a 18 ms (mejora del 99.5%).
- Cache Hit Ratio: pasó del 68% al 99.2%, ya que los índices de la partición activa cabían en memoria.
- Mantenimiento: las operaciones de purga pasaron de bloquear la base de datos durante horas a ejecutarse al instante con un
DROP TABLEde la partición antigua.
Cuándo NO basta con particionar
El particionamiento conlleva trade-offs de ingeniería que deben evaluarse:
- Consultas sin clave de partición: si una consulta no filtra por la clave de partición, el motor debe escanear todas las particiones en paralelo (scatter-gather), empeorando el rendimiento respecto a una tabla monolítica.
- Restricciones de clave única: en PostgreSQL, cualquier restricción
PRIMARY KEYoUNIQUEdebe incluir obligatoriamente la clave de partición. - Migración en producción: convertir una tabla activa de 200M de filas a un esquema particionado requiere estrategias avanzadas (uso de triggers de sincronización, doble escritura en aplicación o migración en batches).
Criterio de decisión
Antes de aprobar una ampliación de infraestructura en tu proveedor cloud, evalúa lo siguiente: ¿más del 80% de tus consultas críticas filtran explícitamente por la misma columna (tiempo, cliente, región)?
- Si la respuesta es SÍ: particionar por ese eje resuelve el problema desde la raíz del diseño de datos.
- Si la respuesta es NO: el particionamiento no solucionará el rendimiento; requerirá rediseñar la estrategia de indexación, desnormalizar o implementar capas de lectura especializadas (CQRS, OLAP).
Lecturas relacionadas
- PostgreSQL: Table Partitioning (documentación oficial)
- Event Sourcing en pagos: la única prueba que aguanta un audit — si tu sistema usa event sourcing, el store de eventos es exactamente el tipo de tabla que crece sin límite y termina necesitando particionamiento
- Race conditions en sistemas de pagos: diagnóstico y patrones para entornos críticos