sqlpostgresqlpartitioningtime-series

Particionamento por intervalo no PostgreSQL: tabelas temporais

Divida uma tabela enorme por intervalo de datas, descarte dados antigos em milissegundos e faca o planejador ler so as particoes necessarias.

3 min de leituraReferencesql · postgresql · partitioning · time-series · performance

Quando uma tabela como orders ou logs cresce para centenas de milhoes de linhas, os indices incham, o VACUUM se arrasta por horas e "apague tudo com mais de um ano" vira um DELETE que derruba a producao. O particionamento por intervalo resolve isso: uma tabela logica e dividida fisicamente em pedacos por um intervalo de valores, quase sempre uma data.

PARTITION BY RANGE: a tabela pai e suas particoes

O particionamento declarativo chegou no PostgreSQL 10 e hoje e a ferramenta padrao. Voce declara uma tabela pai com uma chave de particionamento e depois acopla particoes filhas, cada uma com seu proprio intervalo FROM ... TO.

CREATE TABLE orders (
    id         bigint        GENERATED ALWAYS AS IDENTITY,
    user_id    bigint        NOT NULL,
    amount     numeric(12,2) NOT NULL,
    status     text          NOT NULL,
    created_at timestamptz   NOT NULL
) PARTITION BY RANGE (created_at);

CREATE TABLE orders_2024 PARTITION OF orders
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

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

Detalhes que importam:

  • FROM e inclusivo, TO e exclusivo. Os intervalos nao podem se sobrepor.
  • A chave de particionamento precisa fazer parte de toda UNIQUE e PRIMARY KEY. Por isso aqui a chave primaria e (id, created_at), e nao apenas id.
  • Indices criados no pai sao criados automaticamente em cada particao.

Partition pruning: o planejador le so o necessario

O grande ganho de velocidade e o pruning: quando o WHERE restringe a chave de particionamento, o planejador descarta as particoes que nao podem corresponder ao intervalo.

EXPLAIN
SELECT sum(amount)
FROM orders
WHERE created_at >= '2025-03-01'
  AND created_at <  '2025-04-01';

O plano vai citar apenas orders_2025; a particao de 2024 nem chega a ser aberta. O pruning acontece tanto no planejamento quanto na execucao (para parametros e JOIN). Mas se a consulta nunca menciona created_at (digamos, filtra so por user_id), o Postgres precisa varrer todas as particoes. Essa e a razao classica de "o particionamento nao ajudou em nada".

Descartar dados antigos em milissegundos

A retencao e a funcionalidade matadora. Apagar um ano de linhas com DELETE sao horas de trabalho e um WAL inchado. Com particoes e uma operacao de metadados:

-- Instantaneo: descarta uma particao inteira com dados e indices
DROP TABLE orders_2024;

-- Mais limpo: primeiro desacople para poder arquivar
ALTER TABLE orders DETACH PARTITION orders_2024;
-- ... COPY orders_2024 TO '/archive/orders_2024.csv' ...
DROP TABLE orders_2024;

O DETACH transforma a particao de volta numa tabela independente comum: voce pode arquiva-la, move-la para armazenamento barato ou simplesmente mante-la. A operacao inversa, ATTACH PARTITION, encaixa uma tabela ja pronta como nova particao; use para backfills ou para trazer dados historicos online.

ALTER TABLE orders ATTACH PARTITION orders_2026
    FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');

A particao DEFAULT: uma armadilha para "o resto"

Se uma linha nao se encaixa em nenhum intervalo, o INSERT falha com erro. A rede de seguranca e uma particao DEFAULT que absorve todo o resto:

CREATE TABLE orders_default PARTITION OF orders DEFAULT;

Pegadinha: a particao DEFAULT e conveniente, mas perigosa. Se ela ja contem linhas de 2026, fazer ATTACH de uma nova particao orders_2026 forca uma varredura completa de orders_default para provar que nao ha sobreposicao, e sob um lock. Em series temporais, pre-crie as particoes futuras (com pg_partman ou um cron) e mantenha o DEFAULT vazio como alarme de intervalos faltantes.

MySQL e ClickHouse: onde diferem

  • MySQL: o PARTITION BY RANGE (...) tambem existe, mas a chave deve ser um inteiro ou estar envolvida numa funcao como TO_DAYS(created_at); nao ha DETACH, voce remove uma particao antiga com ALTER TABLE ... DROP PARTITION.
  • ClickHouse: as particoes sao definidas com PARTITION BY toYYYYMM(created_at) sobre o motor MergeTree; elas existem sobretudo para DROP PARTITION e manipulacao de partes inteiras, nao para acelerar filtros, esse papel cabe a chave de ordenacao ORDER BY.

Resumo: projete a chave de particionamento conforme a forma como voce filtra e como apaga os dados. Para logs e pedidos, isso quase sempre e uma data, fatiada por mes ou ano, com as particoes futuras criadas com antecedencia.

Pratique com exercícios reais

Resolva exercícios no treinador de SQL com correção instantânea e dicas.

Abrir o treinador