Particionamento de tabelas no PostgreSQL: quando e como usar

1. Introdução ao particionamento de tabelas

Particionamento de tabelas é uma técnica de design de banco de dados que divide uma tabela grande em partes menores e mais gerenciáveis, chamadas partições. No PostgreSQL, o particionamento nativo foi introduzido na versão 10 e refinado significativamente nas versões 11 e 12. Antes disso, a comunidade utilizava herança de tabelas como solução alternativa, mas o particionamento nativo oferece melhor integração com o otimizador de consultas e funcionalidades como partition pruning.

A principal diferença entre particionamento lógico e herança de tabelas é que, no particionamento nativo, o PostgreSQL gerencia automaticamente a rota dos dados para as partições corretas, enquanto na herança o desenvolvedor precisa criar triggers e regras manuais. Os benefícios incluem: performance superior em queries que filtram pela chave de partição, manutenção simplificada (como descartar partições inteiras em vez de deletar milhões de linhas), e gerenciamento mais eficiente de dados volumosos.

2. Tipos de particionamento suportados no PostgreSQL

O PostgreSQL suporta quatro tipos principais de particionamento:

Particionamento por intervalo (RANGE): Ideal para dados ordenados como datas, timestamps ou IDs sequenciais. Cada partição contém um intervalo contínuo de valores.

CREATE TABLE vendas (
    id SERIAL,
    data_venda DATE NOT NULL,
    cliente_id INTEGER,
    valor NUMERIC
) PARTITION BY RANGE (data_venda);

CREATE TABLE vendas_2024_q1 PARTITION OF vendas
    FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');

CREATE TABLE vendas_2024_q2 PARTITION OF vendas
    FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');

Particionamento por lista (LIST): Adequado para categorias discretas como status, regiões ou tipos de produto.

CREATE TABLE pedidos (
    id SERIAL,
    status TEXT NOT NULL,
    cliente_id INTEGER,
    total NUMERIC
) PARTITION BY LIST (status);

CREATE TABLE pedidos_pendentes PARTITION OF pedidos
    FOR VALUES IN ('pendente', 'aguardando');

CREATE TABLE pedidos_finalizados PARTITION OF pedidos
    FOR VALUES IN ('entregue', 'cancelado');

Particionamento por hash (HASH): Distribui dados uniformemente entre partições sem significado semântico, útil para balanceamento de carga.

CREATE TABLE usuarios (
    id SERIAL,
    nome TEXT,
    email TEXT
) PARTITION BY HASH (id);

CREATE TABLE usuarios_part1 PARTITION OF usuarios
    FOR VALUES WITH (MODULUS 4, REMAINDER 0);

CREATE TABLE usuarios_part2 PARTITION OF usuarios
    FOR VALUES WITH (MODULUS 4, REMAINDER 1);

Particionamento composto (subparticionamento): Combina estratégias, como particionar por ano e depois por mês.

CREATE TABLE logs (
    id SERIAL,
    timestamp TIMESTAMPTZ NOT NULL,
    nivel TEXT,
    mensagem TEXT
) PARTITION BY RANGE (timestamp);

CREATE TABLE logs_2024 PARTITION OF logs
    FOR VALUES FROM ('2024-01-01') TO ('2025-01-01')
    PARTITION BY LIST (nivel);

CREATE TABLE logs_2024_erro PARTITION OF logs_2024
    FOR VALUES IN ('ERROR', 'CRITICAL');

3. Quando usar particionamento: cenários e indicadores

O particionamento é recomendado quando:

  • Tabelas com milhões ou bilhões de registros e crescimento contínuo — a manutenção se torna inviável sem particionamento.
  • Queries que filtram frequentemente por uma chave de partição — o PostgreSQL elimina partições irrelevantes automaticamente.
  • Necessidade de remover dados antigos rapidamente — um DROP TABLE em uma partição é instantâneo comparado a um DELETE que gera vacuum e bloqueia linhas.
  • Cargas de trabalho ETL que processam janelas de dados — como dados diários ou mensais.

Quando NÃO usar particionamento:

  • Tabelas com menos de 10 milhões de registros — o overhead de planejamento pode superar os benefícios.
  • Chaves de partição inadequadas que criam partições desbalanceadas.
  • Dados que raramente são filtrados pela chave de partição.
  • Sistemas com muitas operações de atualização que mudam a chave de partição (rows movendo entre partições).

4. Como implementar particionamento no PostgreSQL

A sintaxe básica começa com a tabela mestre:

CREATE TABLE transacoes (
    id BIGSERIAL,
    data_transacao DATE NOT NULL,
    conta_id INTEGER,
    valor NUMERIC(15,2)
) PARTITION BY RANGE (data_transacao);

Depois, criamos as partições filhas:

CREATE TABLE transacoes_2024_01 PARTITION OF transacoes
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE transacoes_2024_02 PARTITION OF transacoes
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

Para migrar tabelas existentes, usamos ATTACH PARTITION:

-- Primeiro, criar tabela com estrutura idêntica
CREATE TABLE transacoes_antigas (LIKE transacoes INCLUDING ALL);

-- Preencher com dados existentes
INSERT INTO transacoes_antigas SELECT * FROM transacoes_old;

-- Anexar como partição
ALTER TABLE transacoes ATTACH PARTITION transacoes_antigas
    FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');

Ferramentas como pg_partman automatizam esse processo, criando e gerenciando partições automaticamente.

5. Gerenciamento de partições na prática

Adicionando novas partições automaticamente: Podemos usar funções e pg_cron para criar partições futuras:

CREATE OR REPLACE FUNCTION criar_particao_mensal()
RETURNS void AS $$
DECLARE
    mes_inicio DATE;
    mes_fim DATE;
BEGIN
    mes_inicio := date_trunc('month', now() + interval '1 month');
    mes_fim := mes_inicio + interval '1 month';

    EXECUTE format(
        'CREATE TABLE transacoes_%s PARTITION OF transacoes
         FOR VALUES FROM (%L) TO (%L)',
        to_char(mes_inicio, 'YYYY_MM'),
        mes_inicio,
        mes_fim
    );
END;
$$ LANGUAGE plpgsql;

Removendo partições antigas: Extremamente rápido e eficiente:

DROP TABLE transacoes_2023_01;

Dividindo partições: Para ajustar limites:

ALTER TABLE transacoes SPLIT PARTITION transacoes_2024_01
    AT ('2024-01-15')
    INTO (transacoes_2024_01_a, transacoes_2024_01_b);

6. Performance e planos de execução com particionamento

O PostgreSQL utiliza partition pruning para eliminar partições que não contêm dados relevantes. Ao executar uma query com filtro na chave de partição, o planejador analisa os limites e ignora partições desnecessárias:

EXPLAIN ANALYZE SELECT * FROM vendas
WHERE data_venda BETWEEN '2024-01-15' AND '2024-01-20';

O plano mostrará apenas a partição relevante sendo escaneada, resultando em leitura de menos blocos e menor uso de CPU.

Índices locais vs. globais: O PostgreSQL não suporta índices globais (índices únicos em todas as partições). Cada partição pode ter seus próprios índices:

CREATE INDEX idx_vendas_data ON vendas_2024_q1 (data_venda);
CREATE INDEX idx_vendas_cliente ON vendas_2024_q2 (cliente_id);

Cuidados com joins: Joins entre tabelas particionadas funcionam bem se ambas usarem a mesma chave de partição (partition-wise join), recurso disponível desde o PostgreSQL 12.

7. Boas práticas, limitações e armadilhas comuns

Escolha correta da chave de partição: Evite partições desbalanceadas. Por exemplo, particionar por status onde 99% dos registros têm status "ativo" anula os benefícios.

Limitações importantes:
- Não é possível criar chaves estrangeiras entre partições diferentes.
- Não existe constraint UNIQUE global — cada partição tem sua própria unicidade.
- Updates que movem uma linha entre partições são possíveis, mas caros.

Impacto no vacuum: Cada partição é tratada individualmente pelo autovacuum, o que é bom para paralelismo mas requer configuração cuidadosa.

Monitoramento: Use views do sistema para acompanhar:

SELECT
    schemaname,
    tablename,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as tamanho
FROM pg_tables
WHERE tablename LIKE 'vendas_%'
ORDER BY tablename;

8. Conclusão e referências

O particionamento de tabelas no PostgreSQL é uma ferramenta poderosa quando aplicada corretamente. Os pontos críticos são: escolher a chave de partição adequada, monitorar o balanceamento entre partições, e automatizar a criação/remoção de partições. Ferramentas como pg_partman e pg_pathman simplificam significativamente o gerenciamento.

Lembre-se: particionamento não é solução para todos os problemas de performance. Teste sempre com seus dados reais e cargas de trabalho típicas antes de implementar em produção.

Referências