O caminho óbvio, criar a tabela particionada e copiar tudo com INSERT ... SELECT, exige gravar 1,2 TB de novo, manter as duas tabelas sincronizadas durante a cópia e fazer um corte. Existe um atalho: uma tabela comum pode ser anexada como partição de uma tabela particionada com ATTACH PARTITION, desde que seu conteúdo caiba no intervalo declarado. A tabela antiga vira a partição que cobre todo o passado até o fim do mês corrente, e as linhas novas depois dessa data caem nas partições novas. Nada é copiado, e a aplicação continua usando o mesmo nome de tabela.
Antes e depois da troca (as setas mostram para onde cada INSERT e cada consulta vai).
ANTES DEPOIS
aplicacao aplicacao (mesmo SQL, mesmo nome de tabela)
| |
eventos_pedido (1,2 TB) eventos_pedido (pai, sem dados proprios)
PK (id) PK (id, criado_em)
DELETE diario de 12 meses atras |
+-- eventos_pedido_legado ate 2026-12-01 (1,2 TB)
+-- eventos_pedido_2026_12 mes corrente de dados novos
+-- eventos_pedido_2027_01 a frente
+-- eventos_pedido_padrao deve estar sempre vazia
Daqui a 12 meses: DROP TABLE eventos_pedido_legado (milissegundos, sem DELETE, sem VACUUM).O preparo é feito com a tabela em uso. Há duas armadilhas. A primeira é que o ATTACH precisa de índices equivalentes aos da tabela pai, e se não os encontrar, constrói novos com a tabela bloqueada, o que para 1,2 TB significa horas de indisponibilidade. A solução é criá-los antes, com CREATE INDEX CONCURRENTLY, na tabela antiga. A segunda é que o ATTACH também confere se todas as linhas cabem no intervalo, lendo a tabela inteira, a menos que exista um CHECK validado que já prove isso. O CHECK entra como NOT VALID, que é instantâneo, e o VALIDATE lê a tabela sem bloquear leitura nem escrita.
-- Estado de partida: eventos_pedido, 1,2 TB, PK (id), coluna criado_em timestamptz NOT NULL.
-- Nada abaixo bloqueia leitura ou escrita da tabela em producao.
-- 1. Tabela nova, particionada por mes, com a mesma forma. A PK inclui a chave de particao:
-- o Postgres so garante unicidade dentro de cada particao, entao criado_em entra na PK.
CREATE TABLE eventos_pedido_novo (
id bigint NOT NULL,
pedido_id bigint NOT NULL,
tipo text NOT NULL,
payload jsonb NOT NULL,
criado_em timestamptz NOT NULL,
PRIMARY KEY (id, criado_em)
) PARTITION BY RANGE (criado_em);
-- 2. Particoes futuras e uma particao padrao como rede de seguranca (deve ficar sempre vazia).
CREATE TABLE eventos_pedido_2026_12 PARTITION OF eventos_pedido_novo
FOR VALUES FROM ('2026-12-01 00:00:00+00') TO ('2027-01-01 00:00:00+00');
CREATE TABLE eventos_pedido_2027_01 PARTITION OF eventos_pedido_novo
FOR VALUES FROM ('2027-01-01 00:00:00+00') TO ('2027-02-01 00:00:00+00');
CREATE TABLE eventos_pedido_padrao PARTITION OF eventos_pedido_novo DEFAULT;
CREATE INDEX ON eventos_pedido_novo (pedido_id, criado_em);
-- 3. Na tabela antiga: os indices que a particao precisara ter, criados sem bloquear.
-- No ATTACH, o Postgres reaproveita indice equivalente em vez de construir outro.
SET lock_timeout = '2s';
CREATE UNIQUE INDEX CONCURRENTLY eventos_pedido_id_criado_em_uq
ON eventos_pedido (id, criado_em);
CREATE INDEX CONCURRENTLY eventos_pedido_pedido_criado_idx
ON eventos_pedido (pedido_id, criado_em);
-- 4. O CHECK que prova ao ATTACH que todas as linhas cabem no intervalo. Com ele validado,
-- o Postgres nao varre 1,2 TB para conferir. NOT VALID e instantaneo; VALIDATE le a tabela
-- sem impedir leitura nem escrita.
ALTER TABLE eventos_pedido
ADD CONSTRAINT eventos_pedido_limite
CHECK (criado_em < '2026-12-01 00:00:00+00') NOT VALID;
ALTER TABLE eventos_pedido VALIDATE CONSTRAINT eventos_pedido_limite;
Dois detalhes do preparo costumam passar despercebidos. O limite do CHECK precisa ser uma data futura, no fim do mês corrente, porque a tabela antiga continuará recebendo escrita até a troca e o CHECK não pode ser violado nesse intervalo. E os literais de data levam o deslocamento explícito (+00): um timestamptz escrito sem deslocamento é interpretado no fuso da sessão, e uma divergência entre o fuso do script e o do servidor desloca todas as fronteiras de partição em algumas horas.