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:
DROP TABLE orders_2024;
ALTER TABLE orders DETACH PARTITION orders_2024;
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.
Quando uma tabela como
ordersoulogscresce para centenas de milhoes de linhas, os indices incham, oVACUUMse arrasta por horas e "apague tudo com mais de um ano" vira umDELETEque 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:
FROMe inclusivo,TOe exclusivo. Os intervalos nao podem se sobrepor.UNIQUEePRIMARY KEY. Por isso aqui a chave primaria e(id, created_at), e nao apenasid.Partition pruning: o planejador le so o necessario
O grande ganho de velocidade e o pruning: quando o
WHERErestringe 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 eJOIN). Mas se a consulta nunca mencionacreated_at(digamos, filtra so poruser_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
DELETEsao 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
DETACHtransforma 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
INSERTfalha com erro. A rede de seguranca e uma particaoDEFAULTque absorve todo o resto:CREATE TABLE orders_default PARTITION OF orders DEFAULT;MySQL e ClickHouse: onde diferem
PARTITION BY RANGE (...)tambem existe, mas a chave deve ser um inteiro ou estar envolvida numa funcao comoTO_DAYS(created_at); nao haDETACH, voce remove uma particao antiga comALTER TABLE ... DROP PARTITION.PARTITION BY toYYYYMM(created_at)sobre o motorMergeTree; elas existem sobretudo paraDROP PARTITIONe manipulacao de partes inteiras, nao para acelerar filtros, esse papel cabe a chave de ordenacaoORDER 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.