Particionamiento de tablas en PostgreSQL: cuándo y cómo hacerlo correctamente

El particionamiento de tablas en PostgreSQL permite dividir una tabla grande en subtablas más pequeñas llamadas particiones, que se gestionan de forma independiente pero se consultan como si fueran una sola tabla. Está disponible de forma nativa desde PostgreSQL 10, y ha ido mejorando versión a versión. Aquí cubrimos los tres tipos de particionamiento, cuándo tiene sentido usarlo y qué trampas hay que evitar.

Los tres tipos de particionamiento

PARTITION BY RANGE

El más habitual. Divide las filas según rangos de valor en una columna, típicamente una fecha. Una tabla de logs particionada por mes o por año es el ejemplo clásico:

-- Crear tabla particionada por rango de fecha
CREATE TABLE eventos (
  id          BIGSERIAL,
  fecha       TIMESTAMPTZ NOT NULL,
  tipo        TEXT,
  payload     JSONB
) PARTITION BY RANGE (fecha);

-- Crear particiones individuales
CREATE TABLE eventos_2024 PARTITION OF eventos
  FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

CREATE TABLE eventos_2025 PARTITION OF eventos
  FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');

CREATE TABLE eventos_2026 PARTITION OF eventos
  FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');

PARTITION BY LIST

Divide por valores discretos de una columna. Útil cuando tienes, por ejemplo, una columna de región o de país y quieres separar los datos por esas categorías:

CREATE TABLE pedidos (
  id      BIGSERIAL,
  region  TEXT NOT NULL,
  fecha   DATE,
  importe NUMERIC
) PARTITION BY LIST (region);

CREATE TABLE pedidos_es PARTITION OF pedidos FOR VALUES IN ('ES');
CREATE TABLE pedidos_mx PARTITION OF pedidos FOR VALUES IN ('MX');
CREATE TABLE pedidos_otros PARTITION OF pedidos DEFAULT;

PARTITION BY HASH

Distribuye las filas por hash de una columna en un número fijo de particiones. Útil cuando no hay una clave natural de rango ni de lista pero quieres distribuir la carga uniformemente:

CREATE TABLE sesiones (
  id         BIGSERIAL,
  usuario_id BIGINT NOT NULL,
  datos      JSONB
) PARTITION BY HASH (usuario_id);

CREATE TABLE sesiones_0 PARTITION OF sesiones FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE sesiones_1 PARTITION OF sesiones FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE sesiones_2 PARTITION OF sesiones FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE sesiones_3 PARTITION OF sesiones FOR VALUES WITH (MODULUS 4, REMAINDER 3);

Partition pruning: la magia del rendimiento

La gran ventaja de las particiones es que PostgreSQL puede eliminar automáticamente las particiones que no contienen datos relevantes para una query. Esto se llama partition pruning y ocurre cuando el predicado WHERE incluye la clave de partición:

-- PostgreSQL solo lee eventos_2025, ignora el resto
SELECT * FROM eventos
WHERE fecha >= '2025-01-01' AND fecha < '2026-01-01';

Puedes verificar en el plan de ejecución con EXPLAIN que solo aparecen las particiones relevantes. Si aparecen todas, el pruning no está funcionando, quizás porque el predicado usa una función que impide al planificador inferir el rango.

Índices y VACUUM por partición

Cada partición tiene sus propios índices, estadísticas y necesidades de VACUUM. Esto es una ventaja: puedes crear índices distintos en particiones distintas, y el autovacuum trabaja en particiones pequeñas en lugar de en una tabla gigante.

Los índices creados en la tabla padre se propagan automáticamente a las particiones existentes y a las nuevas:

-- Indice en la tabla padre: se crea en todas las particiones
CREATE INDEX idx_eventos_tipo ON eventos (tipo);

-- Indice solo en una particion especifica (si lo necesitas)
CREATE INDEX idx_eventos_2025_payload ON eventos_2025 USING GIN (payload);

Eliminar datos históricos con DROP PARTITION

Una de las ventajas más prácticas del particionamiento es que eliminar datos históricos es casi instantáneo. En lugar de un DELETE que tiene que recorrer millones de filas y generar bloat, simplemente eliminas la partición:

-- Eliminar todos los eventos de 2024 instantaneamente
DROP TABLE eventos_2024;

-- O desconectarla sin eliminar los datos
ALTER TABLE eventos DETACH PARTITION eventos_2024;

Cuándo tiene sentido particionar

El particionamiento añade complejidad: hay que gestionar la creación de nuevas particiones, algunos tipos de queries son más lentos (joins entre tablas particionadas con muchas particiones), y las claves foráneas hacia tablas particionadas tienen limitaciones.

Tiene sentido cuando:

  • La tabla tiene cientos de millones de filas o más y crece de forma continua.
  • Hay un patrón natural de acceso por rango de fecha u otra columna.
  • Necesitas eliminar datos históricos con regularidad.
  • El VACUUM de la tabla completa es demasiado lento o interfiere con la producción.

Para tablas de unos pocos millones de filas, un buen índice B-tree suele ser más que suficiente. Consulta el artículo sobre índices en PostgreSQL de esta misma serie antes de decidir si realmente necesitas particionamiento. Y para verificar que el pruning funciona correctamente, el artículo sobre EXPLAIN ANALYZE te enseña a leer los planes de ejecución con particiones.

Imagen: Pexels / Tima Miroshnichenko

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP