El particionamiento declarativo divide una tabla lógica en tablas físicas más pequeñas. Puede acelerar consultas que filtran períodos concretos, simplificar la eliminación de históricos y acotar el mantenimiento. No mejora automáticamente todas las consultas: la clave debe coincidir con los filtros, la retención y el crecimiento de los datos.

Esta guía utiliza PostgreSQL 16–18, created_at timestamptz y particiones mensuales. Antes de ejecutar comandos, elige un recorrido:

  • Recorrido A: public.events todavía no existe.
  • Recorrido B: public.events ya contiene datos y debe migrarse.

No combines ambos recorridos sobre la misma base.

Cómo funcionan los rangos

Los límites inferiores están incluidos y los superiores excluidos:

FROM ('2026-09-01 00:00:00+00')
TO   ('2026-10-01 00:00:00+00')

La fila de las 00:00 del 1 de septiembre pertenece a septiembre; la de las 00:00 del 1 de octubre pertenece a octubre. Las consultas con filtros compatibles pueden descartar las demás particiones mediante partition pruning. Documentación oficial.

Recorrido A: crear una tabla nueva

Crea el padre con una clave primaria que incluya la clave de partición:

CREATE TABLE public.events (
  id bigint GENERATED BY DEFAULT AS IDENTITY,
  created_at timestamptz NOT NULL,
  account_id bigint NOT NULL,
  event_type text NOT NULL,
  payload jsonb NOT NULL,
  PRIMARY KEY (created_at, id)
) PARTITION BY RANGE (created_at);

Una PRIMARY KEY o restricción UNIQUE del padre debe incluir todas las columnas de partición. Si otras tablas necesitan referenciar solamente id, debes rediseñar esas relaciones antes de continuar.

Crea rangos adyacentes:

CREATE TABLE public.events_2026_08 PARTITION OF public.events
FOR VALUES FROM ('2026-08-01 00:00:00+00')
           TO ('2026-09-01 00:00:00+00');

CREATE TABLE public.events_2026_09 PARTITION OF public.events
FOR VALUES FROM ('2026-09-01 00:00:00+00')
           TO ('2026-10-01 00:00:00+00');

CREATE TABLE public.events_2026_10 PARTITION OF public.events
FOR VALUES FROM ('2026-10-01 00:00:00+00')
           TO ('2026-11-01 00:00:00+00');

Opcionalmente, crea una partición de seguridad:

CREATE TABLE public.events_default
PARTITION OF public.events DEFAULT;

DEFAULT evita fallos de inserción, pero puede ocultar períodos faltantes. Debe permanecer monitorizada y preferentemente vacía.

Define en el padre los índices necesarios:

CREATE INDEX events_account_created_idx
ON public.events (account_id, created_at DESC);

CREATE INDEX events_type_created_idx
ON public.events (event_type, created_at DESC);

PostgreSQL crea índices equivalentes en las particiones. No los agregues por costumbre: conserva solo los que respondan a consultas reales.

Comprueba las fronteras sin conservar datos ficticios:

BEGIN;

INSERT INTO public.events
  (created_at, account_id, event_type, payload)
VALUES
  ('2026-08-31 23:59:59.999999+00',1001,'boundary_a','{}'),
  ('2026-09-01 00:00:00+00',       1001,'boundary_b','{}'),
  ('2026-09-15 12:00:00+00',       1001,'middle','{}')
RETURNING tableoid::regclass AS physical_partition,
          created_at, event_type;

ROLLBACK;

boundary_a debe aparecer en agosto; las otras filas, en septiembre.

Comprueba la jerarquía y la poda:

SELECT relid::regclass, parentrelid::regclass, level, isleaf
FROM pg_partition_tree('public.events');

ANALYZE public.events;

EXPLAIN (COSTS, VERBOSE)
SELECT count(*)
FROM public.events
WHERE created_at >= '2026-09-01 00:00:00+00'
  AND created_at <  '2026-10-01 00:00:00+00';

El plan debería leer septiembre y omitir agosto y octubre. EXPLAIN no ejecuta la consulta; reserva EXPLAIN (ANALYZE, BUFFERS) para una copia o ventana autorizada.

Recorrido B: migrar una tabla existente

PostgreSQL no convierte directamente una tabla normal en particionada. Debes crear una tabla sombra, copiar, validar, detener escrituras, cambiar nombres y recrear dependencias.

Comienza con estimaciones de bajo impacto:

SELECT c.reltuples::bigint AS estimated_rows,
       s.n_live_tup,
       pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size
FROM pg_class c
JOIN pg_stat_user_tables s ON s.relid=c.oid
WHERE c.oid='public.events'::regclass;

Los conteos exactos y agrupaciones mensuales pueden escanear toda la tabla. Ejecútalos en una réplica, una copia restaurada o una ventana controlada.

Guarda el esquema completo e inventaría claves, índices, vistas, triggers, grants, RLS, políticas, secuencias, publicaciones y claves foráneas:

STAMP=$(date -u +%Y%m%dT%H%M%SZ)
WORKDIR="$HOME/postgresql-partition-$STAMP"
install -d -m 0700 "$WORKDIR"

pg_dump --schema-only --dbname=appdb \
  --file="$WORKDIR/appdb-schema-before.sql"

sha256sum "$WORKDIR/appdb-schema-before.sql" \
  > "$WORKDIR/SHA256SUMS"

Crea la tabla sombra sin copiar a ciegas los índices originales:

CREATE TABLE public.events_part (
  LIKE public.events
    INCLUDING DEFAULTS
    INCLUDING GENERATED
    INCLUDING IDENTITY
    INCLUDING STORAGE
    INCLUDING COMMENTS
    INCLUDING CONSTRAINTS
) PARTITION BY RANGE (created_at);

ALTER TABLE public.events_part
ADD PRIMARY KEY (created_at, id);

Si el id original era IDENTITY, INCLUDING IDENTITY crea una secuencia independiente. Si provenía de serial, el default puede seguir apuntando a la secuencia antigua. Compruébalo:

SELECT column_name, is_identity, identity_generation, column_default
FROM information_schema.columns
WHERE table_schema='public'
  AND table_name='events_part'
  AND column_name='id';

SELECT pg_get_serial_sequence('public.events_part','id');

Si no es una identidad y el default referencia la secuencia original, crea una independiente:

CREATE SEQUENCE public.events_part_id_seq
OWNED BY public.events_part.id;

ALTER TABLE public.events_part
ALTER COLUMN id
SET DEFAULT nextval('public.events_part_id_seq'::regclass);

Crea las particiones históricas y futuras y copia por rangos. Esto solo es consistente si las escrituras están detenidas o el rango es demostrablemente inmutable:

INSERT INTO public.events_part
  (id, created_at, account_id, event_type, payload)
SELECT id, created_at, account_id, event_type, payload
FROM public.events
WHERE created_at >= '2026-08-01 00:00:00+00'
  AND created_at <  '2026-09-01 00:00:00+00';

Una copia concurrente simple no captura correctamente UPDATE o DELETE. Una migración online necesita dual-write o replicación con reconciliación. La replicación lógica tampoco replica automáticamente el esquema ni las secuencias. Restricciones oficiales.

Antes del corte, compara conteos por período, duplicados, contenido, routing, pruning, permisos, RLS, triggers y claves. Prepara y ensaya dos manifiestos: uno para recrear dependencias sobre la tabla nueva y otro para devolverlas a la original.

Con las escrituras detenidas:

BEGIN;
SET LOCAL lock_timeout='5s';

LOCK TABLE public.events, public.events_part
IN ACCESS EXCLUSIVE MODE;

ALTER TABLE public.events RENAME TO events_legacy;
ALTER TABLE public.events_part RENAME TO events;

COMMIT;

Las vistas y claves foráneas siguen el OID de la tabla anterior aunque cambie de nombre. Ejecuta el manifiesto previamente ensayado para que apunten a la nueva public.events. No reanudes la aplicación hasta validar lecturas, escrituras, secuencia, constraints, grants y planes.

Si falla una comprobación antes de reanudar escrituras:

BEGIN;
SET LOCAL lock_timeout='5s';

LOCK TABLE public.events, public.events_legacy
IN ACCESS EXCLUSIVE MODE;

ALTER TABLE public.events RENAME TO events_part_failed;
ALTER TABLE public.events_legacy RENAME TO events;

COMMIT;

Después de reanudar escrituras, el rollback deja de ser un simple cambio de nombres: requiere reconciliar los cambios nuevos.

Mantenimiento

Crea particiones futuras antes de necesitarlas. Si utilizas DEFAULT, alerta cuando contenga filas:

SELECT count(*), min(created_at), max(created_at)
FROM public.events_default;

Para retirar históricos:

ALTER TABLE public.events
DETACH PARTITION public.events_2025_08;

Después puedes respaldar y eliminar la tabla separada según la política de retención. Agregar o retirar particiones requiere bloqueos; ensaya siempre el procedimiento.

Cloud Serversby Donweb

Todo el poder de la nube a tus proyectos y aplicaciones.
Alojamiento ultra rápido, escalable y con alta disponibilidad.

  • Performance que te sorprenderá
  • Rápida escalabilidad y sin limitaciones
  • Arquitectura de alta disponibilidad
  • Soporte experto y ejecutivos de cuenta
  • Pagos en tu moneda y facturación local

Descubre la mejor solución de Cloud Hosting