Deep EVM #25: Particionamiento de Tablas en PostgreSQL — Cuando Tu Tabla Supera 10M+ Filas
Engineering Team
Cuándo particionar
PostgreSQL maneja millones de filas sin problemas, pero cuando una tabla crece más allá de ciertos umbrales, el rendimiento se degrada:
- Escaneos secuenciales se vuelven prohibitivamente lentos
- Los índices crecen y ya no caben en memoria
- VACUUM tarda horas y bloquea operaciones
- Backups se vuelven inmanejables
El particionamiento divide una tabla lógica en múltiples tablas físicas, permitiendo al planificador de consultas escanear solo las particiones relevantes.
Tipos de particionamiento
Por rango (RANGE)
Ideal para datos temporales:
CREATE TABLE transactions (
id BIGSERIAL,
block_number BIGINT NOT NULL,
tx_hash BYTEA NOT NULL,
from_addr BYTEA,
to_addr BYTEA,
value NUMERIC,
created_at TIMESTAMPTZ DEFAULT NOW()
) PARTITION BY RANGE (block_number);
CREATE TABLE transactions_0_1m
PARTITION OF transactions
FOR VALUES FROM (0) TO (1000000);
CREATE TABLE transactions_1m_2m
PARTITION OF transactions
FOR VALUES FROM (1000000) TO (2000000);
Por lista (LIST)
Ideal para categorías discretas:
CREATE TABLE pool_events (
id BIGSERIAL,
chain TEXT NOT NULL,
pool_address BYTEA,
event_type TEXT,
data JSONB
) PARTITION BY LIST (chain);
CREATE TABLE pool_events_ethereum
PARTITION OF pool_events FOR VALUES IN ('ethereum');
CREATE TABLE pool_events_arbitrum
PARTITION OF pool_events FOR VALUES IN ('arbitrum');
CREATE TABLE pool_events_optimism
PARTITION OF pool_events FOR VALUES IN ('optimism');
Por hash (HASH)
Distribuye datos uniformemente:
CREATE TABLE address_balances (
address BYTEA NOT NULL,
token BYTEA NOT NULL,
balance NUMERIC
) PARTITION BY HASH (address);
CREATE TABLE address_balances_0
PARTITION OF address_balances FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE address_balances_1
PARTITION OF address_balances FOR VALUES WITH (MODULUS 4, REMAINDER 1);
Partition pruning
El planificador de PostgreSQL elimina automáticamente particiones no relevantes:
EXPLAIN SELECT * FROM transactions WHERE block_number = 1500000;
-- Solo escanea transactions_1m_2m
Para que funcione, la clave de partición debe estar en la cláusula WHERE.
Migración de tabla existente
-- 1. Crear tabla particionada nueva
CREATE TABLE transactions_new (...) PARTITION BY RANGE (block_number);
-- 2. Crear particiones
CREATE TABLE transactions_new_0_1m ...;
-- 3. Copiar datos (en ventana de mantenimiento)
INSERT INTO transactions_new SELECT * FROM transactions;
-- 4. Renombrar
ALTER TABLE transactions RENAME TO transactions_old;
ALTER TABLE transactions_new RENAME TO transactions;
-- 5. Verificar y eliminar antigua
DROP TABLE transactions_old;
Conclusión
El particionamiento de tablas en PostgreSQL es esencial para mantener el rendimiento cuando las tablas crecen más allá de millones de filas. Elegir la estrategia correcta (rango, lista o hash) depende del patrón de acceso a los datos.