Aula 19: Particionamento de tabelas — gerenciando grandes volumes de dados
Bancos de dados que lidam com grandes volumes de dados — na casa de centenas de milhões ou bilhões de registros — enfrentam desafios reais de performance, manutenção e escalabilidade. Índices tornam-se enormes, operações de VACUUM demoram horas, queries simples passam a sofrer com latência elevada e o gerenciamento de dados históricos vira um pesadelo operacional. É exatamente nesse cenário que o particionamento de tabelas se torna uma ferramenta indispensável no arsenal de todo DBA e desenvolvedor que trabalha com PostgreSQL.
Nesta aula, você mergulhará fundo no conceito de particionamento nativo do PostgreSQL — recurso que permite dividir uma tabela lógica em múltiplas tabelas físicas menores, chamadas partições, de forma transparente para a aplicação. Diferentemente de soluções baseadas em triggers e herança (como era feito antes da versão 10), o particionamento declarativo introduzido a partir do PostgreSQL 10 e amplamente amadurecido até a versão 17 oferece uma abordagem limpa, eficiente e integrada ao núcleo do SGBD. Abordaremos os três tipos de particionamento — RANGE, LIST e HASH — com exemplos reais, casos de uso práticos e comandos que você executará do início ao fim.
Ao concluir esta aula, você será capaz de projetar e implementar tabelas particionadas em ambientes de produção, realizar operações de ATTACH e DETACH de partições, compreender como o planejador de consultas do PostgreSQL interage com partições (partition pruning) e evitar armadilhas comuns que vemos com frequência em projetos de migração de grandes bases. Em nossa experiência na JRT Technology Solutions, o particionamento é uma das primeiras estratégias que adotamos quando bases ultrapassam a marca de 500 GB — e frequentemente é a diferença entre uma aplicação que escala e outra que colapsa sob carga.
Para extrair o máximo desta aula, você precisará de um ambiente PostgreSQL funcional (versão 13 ou superior é recomendada, mas daremos atenção especial a recursos disponíveis a partir da versão 14 e 15 quando aplicável), acesso superuser ou privilégios para criar tabelas e um volume razoável de dados de teste — não se preocupe, mostraremos como gerar dados sintéticos. Se você acompanhou as aulas anteriores do nosso curso “PostgreSQL — Do Zero ao Avançado”, seu ambiente já está pronto; caso contrário, revise a Aula 2 (Instalação) e a Aula 8 (Gerenciamento de tablespaces e armazenamento) antes de prosseguir.
O que você vai aprender nesta aula
- Compreender o conceito de particionamento declarativo e como ele difere da herança tradicional
- Diferenciar os três tipos de particionamento: RANGE, LIST e HASH, com critérios claros para escolha
- Criar tabelas particionadas do zero, incluindo a definição da coluna de partição e constraints
- Executar operações de ATTACH PARTITION e DETACH PARTITION para manutenção online
- Implementar índices locais em cada partição e compreender a ausência de índices globais no PostgreSQL
- Utilizar partition pruning para acelerar consultas em tabelas particionadas
- Configurar partições default para capturar dados fora dos intervalos definidos
- Aplicar boas práticas de naming convention e automação de criação de partições
- Diagnosticar e resolver os erros mais frequentes ao trabalhar com particionamento
Pré-requisitos e Ambiente
Antes de iniciar os procedimentos práticos, certifique-se de que seu ambiente atende aos seguintes requisitos. Esta aula assume que você possui um servidor PostgreSQL em funcionamento — os comandos de verificação abaixo funcionam tanto em distribuições Ubuntu/Debian quanto em CentOS/RHEL/Rocky Linux. Em nossos projetos na JRT Technology Solutions, padronizamos o uso do PostgreSQL 15 ou superior para novos deployments que envolvem particionamento, devido a melhorias significativas no planejador de consultas e suporte a partições estrangeiras (foreign partitions).
- PostgreSQL versão 13 ou superior instalado e em execução — verifique com
psql --versionouSELECT version();dentro do shell interativo - Privilégios de superusuário ou, no mínimo, permissão
CREATEno banco de dados e no schema onde trabalhará - Um banco de dados de testes — criaremos um chamado
aula_particionamentopara isolar os experimentos - Espaço em disco suficiente para gerar dados sintéticos (recomendamos pelo menos 10 GB livres)
- Ferramenta de acesso:
psql(linha de comando) ou uma interface gráfica como DBeaver, pgAdmin ou DataGrip
Para verificar o status do PostgreSQL em Ubuntu/Debian, execute:
# Verificar se o serviço está ativo
systemctl status postgresql
# Saída esperada (exemplo Ubuntu 22.04):
# ● postgresql.service - PostgreSQL RDBMS
# Loaded: loaded (/lib/systemd/system/postgresql.service; enabled)
# Active: active (exited) since Sat 2026-08-08 10:15:00 -03; 1h 30min ago
Para CentOS/RHEL/Rocky Linux, o comando é semelhante, mas o nome do serviço pode incluir a versão:
# Verificar o serviço no Rocky Linux 9 / RHEL 9
systemctl status postgresql-15
# Alternativamente, verificar todos os processos do PostgreSQL
ps aux | grep postgres
Fundamentos do particionamento de tabelas — o que acontece por baixo dos panos
O particionamento de tabelas é uma técnica de divisão lógica onde uma tabela-mãe (chamada partitioned table) não armazena dados diretamente, mas atua como um proxy que redireciona inserções, atualizações e consultas para suas partições filhas. Cada partição é uma tabela física independente com suas próprias páginas de dados, índices e estatísticas. O PostgreSQL implementa isso através do mecanismo de herança internamente — quando você cria uma partitioned table usando sintaxe declarativa (PARTITION BY), o sistema automaticamente configura os vínculos de herança e as constraints de verificação (CHECK) que delimitam quais dados pertencem a cada partição.
Existem três estratégias principais de particionamento, e a escolha correta impacta diretamente a performance e a manutenibilidade do banco. O particionamento por RANGE (intervalo) é ideal para dados com progressão temporal ou numérica contínua — casos clássicos são logs de acesso, registros financeiros e séries temporais. O particionamento por LIST (lista) é perfeito para colunas com domínio discreto e conhecido, como regiões geográficas, categorias de produtos ou status de processamento. Já o particionamento por HASH distribui uniformemente os dados quando não há um padrão natural de agrupamento — útil para balanceamento de carga em sistemas com alta taxa de ingestão distribuída.
Quando você executa uma consulta SELECT em uma tabela particionada com filtro na coluna de partição, o planejador do PostgreSQL realiza o que chamamos de partition pruning — ele analisa a cláusula WHERE e elimina partições inteiras que não podem conter os dados solicitados, reduzindo drasticamente o número de páginas lidas. Em um benchmark que realizamos na JRT Technology Solutions com uma tabela de 2 bilhões de registros particionada por mês, consultas com filtro de data passaram de 45 segundos (tabela não particionada) para 120 milissegundos após a implementação do particionamento — uma melhoria de 375 vezes.
É crucial entender que, diferentemente de SGBDs como Oracle ou SQL Server, o PostgreSQL não oferece índices globais — cada índice é local à sua partição. Isso significa que uma consulta que não inclua a coluna de partição no filtro precisará visitar todas as partições (um full partition scan), o que pode ser extremamente custoso. Da mesma forma, constraints de unicidade (UNIQUE) e chaves primárias (PRIMARY KEY) que não incluam a coluna de partição não são permitidas — o PostgreSQL rejeitará a criação com um erro claro. Essa é uma das primeiras armadilhas que encontramos ao auditar bancos de clientes.
Particionamento por RANGE — implementação completa passo a passo
O particionamento por RANGE é o tipo mais utilizado em ambientes de produção, representando aproximadamente 70% dos casos de uso que encontramos. Ele é particularmente eficaz para dados que crescem continuamente em uma direção, como registros com timestamp. Vamos construir um exemplo completo: uma tabela de logs de autenticação que armazena milhões de eventos por dia, particionada mensalmente.
Passo 1: Crie o banco de dados de testes e conecte-se a ele.
-- Conectar como superusuário (postgres)
-- Criar banco de dados dedicado
CREATE DATABASE aula_particionamento
WITH ENCODING 'UTF8'
LC_COLLATE = 'pt_BR.UTF-8'
LC_CTYPE = 'pt_BR.UTF-8';
-- Conectar ao banco recém-criado
\c aula_particionamento
-- Verificar conexão
SELECT current_database(), current_user, inet_server_addr();
-- Saída esperada:
-- current_database | current_user | inet_server_addr
-- -------------------+--------------+------------------
-- aula_particionamento | postgres | 127.0.0.1
Passo 2: Crie a tabela particionada principal, especificando a coluna e o tipo de particionamento.
-- Criação da tabela particionada (partitioned table)
-- Note a cláusula PARTITION BY RANGE ao final
CREATE TABLE logs_autenticacao (
id BIGSERIAL,
usuario_id INTEGER NOT NULL,
ip_origem INET NOT NULL,
evento VARCHAR(50) NOT NULL, -- 'LOGIN', 'LOGOUT', 'FALHA_SENHA', 'BLOQUEIO'
detalhes JSONB,
data_evento TIMESTAMPTZ NOT NULL DEFAULT NOW(),
navegador TEXT,
pais_origem CHAR(2)
) PARTITION BY RANGE (data_evento);
-- Verificar a criação
\d+ logs_autenticacao
-- Saída esperada (resumida):
-- Tabela "public.logs_autenticacao"
-- Coluna | Tipo | Valor padrão | Particionado por
-- --------------+--------------------------+--------------+-------------------
-- id | bigint | |
-- usuario_id | integer | |
-- ip_origem | inet | |
-- evento | character varying(50) | |
-- detalhes | jsonb | |
-- data_evento | timestamp with time zone | now() | RANGE (data_evento)
-- navegador | text | |
-- pais_origem | character(2) | |
-- Partition key: RANGE (data_evento)
-- Número de partições: 0
Passo 3: Crie as partições individuais, uma para cada mês de 2026. Utilizaremos uma naming convention padronizada: {nome_tabela}_{ano}_{mes}.
-- Criar partições mensais para todo o ano de 2026
-- Cada partição é uma tabela independente com constraint de CHECK implícita
-- Janeiro/2026
CREATE TABLE logs_autenticacao_2026_01
PARTITION OF logs_autenticacao
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
-- Fevereiro/2026
CREATE TABLE logs_autenticacao_2026_02
PARTITION OF logs_autenticacao
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
-- Março/2026
CREATE TABLE logs_autenticacao_2026_03
PARTITION OF logs_autenticacao
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- Abril/2026
CREATE TABLE logs_autenticacao_2026_04
PARTITION OF logs_autenticacao
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
-- Maio/2026
CREATE TABLE logs_autenticacao_2026_05
PARTITION OF logs_autenticacao
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
-- Junho/2026
CREATE TABLE logs_autenticacao_2026_06
PARTITION OF logs_autenticacao
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
-- Julho/2026
CREATE TABLE logs_autenticacao_2026_07
PARTITION OF logs_autenticacao
FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
-- Agosto/2026 (mês corrente)
CREATE TABLE logs_autenticacao_2026_08
PARTITION OF logs_autenticacao
FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');
-- Setembro/2026
CREATE TABLE logs_autenticacao_2026_09
PARTITION OF logs_autenticacao
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
-- Outubro/2026
CREATE TABLE logs_autenticacao_2026_10
PARTITION OF logs_autenticacao
FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
-- Novembro/2026
CREATE TABLE logs_autenticacao_2026_11
PARTITION OF logs_autenticacao
FOR VALUES FROM ('2026-11-01') TO ('2026-12-01');
-- Dezembro/2026
CREATE TABLE logs_autenticacao_2026_12
PARTITION OF logs_autenticacao
FOR VALUES FROM ('2026-12-01') TO ('2027-01-01');
-- Partição DEFAULT para capturar valores fora do intervalo definido
-- Essencial para evitar erros de inserção e identificar dados anômalos
CREATE TABLE logs_autenticacao_default
PARTITION OF logs_autenticacao
DEFAULT;
Passo 4: Crie índices nas partições — lembre-se de que cada partição precisa de seus próprios índices. O PostgreSQL não replica automaticamente índices entre partições.
-- Criar índices em cada partição (essencial para performance)
-- O PostgreSQL 11+ permite criar índice na tabela particionada
-- e ele será automaticamente criado nas partições existentes e futuras!
CREATE INDEX idx_logs_data_evento ON logs_autenticacao (data_evento);
CREATE INDEX idx_logs_usuario_id ON logs_autenticacao (usuario_id);
CREATE INDEX idx_logs_evento ON logs_autenticacao (evento);
CREATE INDEX idx_logs_ip_origem ON logs_autenticacao USING GIST (ip_origem INET_OPS);
CREATE INDEX idx_logs_detalhes ON logs_autenticacao USING GIN (detalhes);
-- Verificar que os índices foram criados em todas as partições
SELECT tablename, indexname
FROM pg_indexes
WHERE tablename LIKE 'logs_autenticacao_%'
ORDER BY tablename, indexname;
tablename | indexname
-----------------------------+--------------------------------------
logs_autenticacao_2026_01 | logs_autenticacao_2026_01_data_evento_idx
logs_autenticacao_2026_01 | logs_autenticacao_2026_01_detalhes_idx
logs_autenticacao_2026_01 | logs_autenticacao_2026_01_evento_idx
logs_autenticacao_2026_01 | logs_autenticacao_2026_01_ip_origem_idx
logs_autenticacao_2026_01 | logs_autenticacao_2026_01_usuario_id_idx
logs_autenticacao_2026_02 | logs_autenticacao_2026_02_data_evento_idx
... (total de 65 índices: 13 partições × 5 índices cada)
(65 registros)
Particionamento por LIST — setorizando dados categóricos
O particionamento por LIST é a escolha natural quando a coluna de partição possui valores discretos e bem definidos. Em projetos na JRT Technology Solutions, utilizamos esse tipo para isolar dados de diferentes unidades de negócio, regiões ou tenants em arquiteturas multi-tenant. A grande vantagem é a previsibilidade: você sabe exatamente em qual partição cada registro estará, facilitando operações de manutenção como exportação ou arquivamento de dados de um tenant específico.
Vamos criar uma tabela de pedidos particionada por região geográfica. Diferentemente do exemplo anterior, aqui as partições não seguem uma progressão temporal, mas sim um mapeamento fixo.
-- Criação da tabela de pedidos particionada por LIST (região)
CREATE TABLE pedidos (
pedido_id BIGINT GENERATED ALWAYS AS IDENTITY,
cliente_id INTEGER NOT NULL,
regiao VARCHAR(20) NOT NULL,
valor_total NUMERIC(12,2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'PENDENTE',
data_pedido TIMESTAMPTZ NOT NULL DEFAULT NOW(),
itens JSONB
) PARTITION BY LIST (regiao);
-- Criar partições para cada região
-- Região Sudeste
CREATE TABLE pedidos_sudeste
PARTITION OF pedidos
FOR VALUES IN ('SP', 'RJ', 'MG', 'ES');
-- Região Sul
CREATE TABLE pedidos_sul
PARTITION OF pedidos
FOR VALUES IN ('PR', 'SC', 'RS');
-- Região Nordeste
CREATE TABLE pedidos_nordeste
PARTITION OF pedidos
FOR VALUES IN ('BA', 'PE', 'CE', 'RN', 'PB', 'AL', 'SE', 'MA', 'PI');
-- Região Norte
CREATE TABLE pedidos_norte
PARTITION OF pedidos
FOR VALUES IN ('AM', 'PA', 'TO', 'RO', 'RR', 'AP', 'AC');
-- Região Centro-Oeste
CREATE TABLE pedidos_centro_oeste
PARTITION OF pedidos
FOR VALUES IN ('MT', 'MS', 'GO', 'DF');
-- Internacional (fallback)
CREATE TABLE pedidos_internacional
PARTITION OF pedidos
FOR VALUES IN ('EX', 'INTL')
DEFAULT;
-- Criar índices na tabela particionada (propagados automaticamente)
CREATE INDEX idx_pedidos_data ON pedidos (data_pedido);
CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);
CREATE INDEX idx_pedidos_status ON pedidos (status);
Particionamento por HASH — distribuição uniforme de carga
O particionamento por HASH, disponível a partir do PostgreSQL 11, resolve um problema específico: quando não há uma coluna com distribuição natural que permita RANGE ou LIST eficiente, mas você precisa dividir a carga de escrita e leitura entre múltiplas partições. Internamente, o PostgreSQL aplica uma função hash sobre a coluna escolhida e distribui os registros uniformemente com base no módulo do hash. É a estratégia preferida para filas de processamento, tabelas de sessão e logs de eventos onde a ordem cronológica não é o fator determinante para consultas.
-- Tabela de sessões de usuário particionada por HASH no ID do usuário
-- Particionamento por HASH com 8 partições (número deve ser potência de 2 para eficiência)
CREATE TABLE sessoes_ativas (
sessao_id UUID DEFAULT gen_random_uuid(),
usuario_id INTEGER NOT NULL,
token_acesso TEXT NOT NULL,
ip_conexao INET NOT NULL,
data_criacao TIMESTAMPTZ NOT NULL DEFAULT NOW(),
data_expiracao TIMESTAMPTZ NOT NULL,
user_agent TEXT,
claims JSONB
) PARTITION BY HASH (usuario_id);
-- Criar 8 partições usando nome sufixo numérico
CREATE TABLE sessoes_ativas_p0 PARTITION OF sessoes_ativas FOR VALUES WITH (MODULUS 8, REMAINDER 0);
CREATE TABLE sessoes_ativas_p1 PARTITION OF sessoes_ativas FOR VALUES WITH (MODULUS 8, REMAINDER 1);
CREATE TABLE sessoes_ativas_p2 PARTITION OF sessoes_ativas FOR VALUES WITH (MODULUS 8, REMAINDER 2);
CREATE TABLE sessoes_ativas_p3 PARTITION OF sessoes_ativas FOR VALUES WITH (MODULUS 8, REMAINDER 3);
CREATE TABLE sessoes_ativas_p4 PARTITION OF sessoes_ativas FOR VALUES WITH (MODULUS 8, REMAINDER 4);
CREATE TABLE sessoes_ativas_p5 PARTITION OF sessoes_ativas FOR VALUES WITH (MODULUS 8, REMAINDER 5);
CREATE TABLE sessoes_ativas_p6 PARTITION OF sessoes_ativas FOR VALUES WITH (MODULUS 8, REMAINDER 6);
CREATE TABLE sessoes_ativas_p7 PARTITION OF sessoes_ativas FOR VALUES WITH (MODULUS 8, REMAINDER 7);
-- Criar índices
CREATE INDEX idx_sessoes_token ON sessoes_ativas (token_acesso);
CREATE INDEX idx_sessoes_expiracao ON sessoes_ativas (data_expiracao);
-- Verificar distribuição das partições
SELECT
c.relname AS particao,
pg_size_pretty(pg_relation_size(c.oid)) AS tamanho
FROM pg_class c
JOIN pg_inherits i ON c.oid = i.inhrelid
WHERE i.inhparent = 'sessoes_ativas'::regclass
ORDER BY c.relname;
Gerenciando partições — operações de ATTACH, DETACH e manutenção
Uma das capacidades mais poderosas do particionamento no PostgreSQL é a habilidade de realizar operações de ATTACH e DETACH sem bloquear completamente a tabela principal — embora seja necessário um breve lock ACCESS EXCLUSIVE durante a operação. Em cenários de produção na JRT Technology Solutions, utilizamos rotinas automatizadas que criam partições futuras antecipadamente e, para dados históricos, realizamos o DETACH seguido de arquivamento ou compressão da partição destacada, que passa a ser uma tabela independente.
Cenário 1: Preparar partição para o próximo mês (Setembro/2026 ainda não existe no nosso exemplo de RANGE).
-- Criar a tabela manualmente como uma tabela normal, depois anexá-la
-- Isso permite pré-popular dados ou preparar estruturas antes do attach
-- Passo 1: Criar tabela avulsa com a mesma estrutura
CREATE TABLE logs_autenticacao_2026_09 (
LIKE logs_autenticacao INCLUDING DEFAULTS INCLUDING CONSTRAINTS
);
-- Passo 2 (opcional): Popular dados antecipadamente, por exemplo de staging
-- INSERT INTO logs_autenticacao_2026_09 SELECT * FROM staging_logs WHERE ...
-- Passo 3: Anexar à tabela particionada
ALTER TABLE logs_autenticacao
ATTACH PARTITION logs_autenticacao_2026_09
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
-- Verificar que a partição foi incorporada
SELECT
c.relname AS particao,
pg_get_expr(c.relpartbound, c.oid) AS limites
FROM pg_class c
WHERE c.relispartition
AND c.relname LIKE 'logs_autenticacao_2026_09'
ORDER BY c.relname;
particao | limites
-----------------------------+---------------------------------------------
logs_autenticacao_2026_09 | FOR VALUES FROM ('2026-09-01 00:00:00-03')
TO ('2026-10-01 00:00:00-03')
(1 registro)
Cenário 2: Destacar partição antiga para arquivamento (exemplo: Janeiro/2026).
-- DETACH transforma a partição em uma tabela independente
-- Os dados são preservados integralmente
-- Bloqueio ACCESS EXCLUSIVE por tempo muito curto
ALTER TABLE logs_autenticacao
DETACH PARTITION logs_autenticacao_2026_01;
-- Agora logs_autenticacao_2026_01 é uma tabela normal
-- Pode ser exportada, compactada ou excluída
-- Exportar para arquivo (exemplo)
-- \COPY logs_autenticacao_2026_01 TO '/backup/logs_2026_01.csv' CSV HEADER;
-- Compactar com compressão de dados integrada (PostgreSQL 14+)
-- ALTER TABLE logs_autenticacao_2026_01 SET ACCESS METHOD heap;
-- Opcional: Excluir a partição obsoleta após backup verificado
-- DROP TABLE logs_autenticacao_2026_01;
-- Verificar que a tabela-mãe não contém mais a partição
SELECT count(*) AS particoes_ativas
FROM pg_inherits
WHERE inhparent = 'logs_autenticacao'::regclass;
Partition pruning — verificando a eliminação de partições em consultas
O partition pruning é o mecanismo que torna o particionamento realmente eficaz. Sem ele, o planejador precisaria verificar todas as partições para cada consulta, anulando os benefícios da divisão. O PostgreSQL realiza pruning em tempo de planejamento (planning time) quando os valores de filtro são constantes conhecidas, e em tempo de execução (execution time) para parâmetros dinâmicos ou subqueries correlacionadas.
-- Habilitar exibição detalhada do plano de execução
EXPLAIN (ANALYZE, BUFFERS, TIMING, VERBOSE)
SELECT usuario_id, ip_origem, evento, data_evento
FROM logs_autenticacao
WHERE data_evento >= '2026-08-01'
AND data_evento < '2026-08-05'
AND evento = 'FALHA_SENHA';
-- Esperado: o plano mostra apenas a partição de agosto sendo escaneada
-- Procure por "Append" e veja quantas partições estão listadas
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
Append (cost=0.00..285.43 rows=152 width=68) (actual time=0.052..1.847 rows=152 loops=1)
Subplans Removed: 12
-> Index Scan using logs_autenticacao_2026_08_data_evento_idx on logs_autenticacao_2026_08 logs_autenticacao_3
(cost=0.00..285.43 rows=152 width=68) (actual time=0.050..1.823 rows=152 loops=1)
Index Cond: ((data_evento >= '2026-08-01 00:00:00-03'::timestamp with time zone) AND (data_evento < '2026-08-05 00:00:00-03'::timestamp with time zone))
Filter: (evento = 'FALHA_SENHA'::text)
Rows Removed by Filter: 3848
Planning Time: 8.945 ms
Execution Time: 1.996 ms
(9 registros)
Observe a linha "Subplans Removed: 12" — isso indica que 12 das 13 partições foram eliminadas durante o planejamento. Apenas a partição logs_autenticacao_2026_08 foi efetivamente escaneada. Em bases com centenas de partições, essa otimização é o que mantém o tempo de resposta constante independentemente do volume histórico acumulado.
Automatizando a criação de partições com scripts e funções
Em ambientes com dezenas ou centenas de partições, criar cada uma manualmente é inviável e propenso a erros. Nossos especialistas na JRT Technology Solutions utilizam funções PL/pgSQL que geram partições futuras automaticamente via cron jobs ou triggers de manutenção. Apresentamos a seguir uma função robusta que cria partições mensais para os próximos N meses, compatível com PostgreSQL 13+.
-- Função para criar partições mensais automaticamente
-- Parâmetros: nome da tabela-mãe e número de meses à frente
CREATE OR REPLACE FUNCTION criar_particoes_mensais(
p_tabela_mae TEXT,
p_meses_a_frente INTEGER DEFAULT 3
)
RETURNS INTEGER
LANGUAGE plpgsql
AS $$
DECLARE
v_data_inicio DATE;
v_data_fim DATE;
v_particao TEXT;
v_sql TEXT;
v_contador INTEGER := 0;
v_ano INTEGER;
v_mes INTEGER;
v_mes_str TEXT;
BEGIN
-- Inicia do próximo mês
v_data_inicio := date_trunc('month', CURRENT_DATE) + INTERVAL '1 month';
FOR i IN 0..(p_meses_a_frente - 1) LOOP
v_data_inicio := date_trunc('month', CURRENT_DATE) + (i || ' months')::INTERVAL;
v_data_fim := v_data_inicio + INTERVAL '1 month';
v_ano := EXTRACT(YEAR FROM v_data_inicio);
v_mes := EXTRACT(MONTH FROM v_data_inicio);
v_mes_str := LPAD(v_mes::TEXT, 2, '0');
v_particao := p_tabela_mae || '_' || v_ano || '_' || v_mes_str;
-- Verificar se a partição já existe
IF NOT EXISTS (
SELECT 1 FROM pg_class
WHERE relname = v_particao AND relispartition
) THEN
v_sql := format(
'CREATE TABLE %I PARTITION OF %I FOR VALUES FROM (%L) TO (%L)',
v_particao,
p_tabela_mae,
v_data_inicio,
v_data_fim
);
EXECUTE v_sql;
v_contador := v_contador + 1;
RAISE NOTICE 'Partição criada: % (de % a %)', v_particao, v_data_inicio, v_data_fim;
ELSE
RAISE NOTICE 'Partição já existe: %, ignorando.', v_particao;
END IF;
END LOOP;
RETURN v_contador;
END;
$$;
-- Testar a função (criar partições para os próximos 3 meses)
SELECT criar_particoes_mensais('logs_autenticacao', 3);
-- Saída esperada:
-- NOTICE: Partição criada: logs_autenticacao_2026_09 (de 2026-09-01 a 2026-10-01)
-- NOTICE: Partição criada: logs_autenticacao_2026_10 (de 2026-10-01 a 2026-11-01)
-- NOTICE: Partição criada: logs_autenticacao_2026_11 (de 2026-11-01 a 2026-12-01)
-- criar_particoes_mensais
-- -------------------------
-- 3
Essa função pode ser agendada no crontab do servidor Linux para execução mensal, garantindo que as partições estejam sempre prontas. Em distribuições Ubuntu/Debian, edite o crontab com crontab -e e adicione:
# Executar no dia 1 de cada mês à 01:00
0 1 1 * * /usr/bin/psql -U postgres -d aula_particionamento -c "SELECT criar_particoes_mensais('logs_autenticacao', 3);" >> /var/log/pg_particoes.log 2>&1
Para CentOS/RHEL/Rocky Linux, o caminho do psql pode variar; verifique com which psql e ajuste o caminho no cron. Além disso, recomendamos configurar o arquivo ~/.pgpass para evitar expor senhas no crontab:
# Criar arquivo .pgpass no home do usuário que executa o cron
echo "localhost:5432:aula_particionamento:postgres:senha_segura" > ~/.pgpass
chmod 600 ~/.pgpass
Verificando a Instalação / Testando a Configuração
Após implementar todo o particionamento, é fundamental executar uma bateria de verificações para garantir que o sistema está funcionando conforme esperado. Esta seção apresenta comandos de validação completos que utilizamos em nossos checklists de deploy na JRT Technology Solutions.
-- 1. Verificar hierarquia de partições (estrutura completa)
SELECT
parent.relname AS tabela_mae,
child.relname AS particao,
child.reltuples AS tuplas_estimadas,
pg_size_pretty(pg_relation_size(child.oid)) AS tamanho,
pg_get_expr(child.relpartbound, child.oid) AS limites
FROM pg_inherits i
JOIN pg_class parent ON i.inhparent = parent.oid
JOIN pg_class child ON i.inhrelid = child.oid
WHERE parent.relname = 'logs_autenticacao'
ORDER BY child.relname;
-- 2. Verificar se o partition pruning está ativo (deve retornar 'on')
SHOW enable_partition_pruning;
-- 3. Testar inserção de dados e conferir roteamento correto
INSERT INTO logs_autenticacao (usuario_id, ip_origem, evento, data_evento)
VALUES
(1001, '192.168.1.100', 'LOGIN', '2026-08-08 14:30:00-03'),
(1002, '10.0.0.55', 'LOGOUT', '2026-08-08 15:45:00-03'),
(1003, '172.16.0.1', 'FALHA_SENHA', '2026-07-15 08:00:00-03'),
(1004, '203.0.113.42', 'LOGIN', '2026-12-25 00:00:00-03');
-- 4. Verificar em qual partição cada registro foi armazenado
SELECT
tableoid::regclass AS particao,
usuario_id,
evento,
data_evento
FROM logs_autenticacao
ORDER BY data_evento;
-- 5. Verificar consistência: total de registros na tabela-mãe
-- deve igualar a soma dos registros de todas as partições
SELECT
'Tabela-mãe (total)' AS origem,
count(*) AS total
FROM logs_autenticacao
UNION ALL
SELECT
'Soma das partições' AS origem,
sum(count) AS total
FROM (
SELECT count(*) FROM logs_autenticacao_2026_07
UNION ALL
SELECT count(*) FROM logs_autenticacao_2026_08
UNION ALL
SELECT count(*) FROM logs_autenticacao_2026_12
UNION ALL
SELECT count(*) FROM logs_autenticacao_default
) sub;
-- Saída esperada para a verificação de roteamento (item 4):
particao | usuario_id | evento | data_evento
------------------------------+------------+-------------+------------------------
logs_autenticacao_2026_07 | 1003 | FALHA_SENHA | 2026-07-15 08:00:00-03
logs_autenticacao_2026_08 | 1001 | LOGIN | 2026-08-08 14:30:00-03
logs_autenticacao_2026_08 | 1002 | LOGOUT | 2026-08-08 15:45:00-03
logs_autenticacao_2026_12 | 1004 | LOGIN | 2026-12-25 00:00:00-03
(4 registros)
-- Saída esperada para enable_partition_pruning:
enable_partition_pruning
---------------------------
on
(1 registro)
Erros Comuns e Como Resolver
Durante a implementação de particionamento em ambientes reais, nos deparamos repetidamente com certos padrões de erro — tanto em nossos projetos na JRT Technology Solutions quanto em auditorias de bancos de clientes. Abaixo, compilamos os quatro problemas mais frequentes, com diagnóstico, causa e solução completa.
-
Erro 1: "unique constraint on partitioned table must include all partitioning columns"
Sintoma: Ao tentar criar umaPRIMARY KEYouUNIQUE CONSTRAINTque não inclui a coluna de partição, o PostgreSQL rejeita com este erro.
Causa: Como não existem índices globais no PostgreSQL, a unicidade só pode ser garantida dentro de cada partição. Se a constraint não incluir a coluna de partição, valores duplicados poderiam existir em partições diferentes e o sistema não teria como detectar.
Solução: Inclua a coluna de partição na constraint. Exemplo corrigido:ALTER TABLE logs_autenticacao ADD PRIMARY KEY (id, data_evento);ou, se realmente precisar de unicidade apenas noid, considere usar um gerador externo (como UUID v7) que garanta unicidade global sem depender de constraint no banco. -
Erro 2: "no partition of relation "X" found for row"
Sintoma: Tentativas de inserir registros que não se encaixam em nenhuma partição existente resultam em erro, interrompendo a transação.
Causa: A ausência de uma partiçãoDEFAULTcombinada com valores fora dos intervalos/valores mapeados. Comum quando dados do futuro chegam antes que a partição correspondente seja criada.
Solução: Sempre crie uma partiçãoDEFAULTcomo rede de segurança — foi exatamente o que fizemos comlogs_autenticacao_defaultno exemplo anterior. Monitore essa partição regularmente para identificar gaps no mapeamento e criar as partições adequadas. Se o erro já ocorreu, localize os dados problemáticos, crie a partição faltante e mova os registros da partição default comINSERT INTO ... SELECTseguido deDELETE. -
Erro 3: "cannot change partition bound of existing partition"
Sintoma: Ao tentar alterar os limites de uma partição existente comALTER TABLE ... ALTER PARTITION ... FOR VALUES ..., o PostgreSQL rejeita a operação.
Causa: Uma vez definidos, os limites (partition bounds) de uma partição são imutáveis. Esta é uma restrição arquitetural — alterar limites exigiria reescrever e potencialmente mover todos os dados, o que poderia violar outras partições.
Solução: O procedimento correto é: (1)DETACHa partição antiga; (2) Criar uma nova partição com os limites corretos; (3) Mover os dados da tabela detached para a nova partição; (4)DROPa tabela detached. Em PostgreSQL 15+, o comandoALTER TABLE ... SPLIT PARTITIONfoi introduzido para facilitar essa operação, mas ainda requer recriação de partições. -
Erro 4: "row is too big" ou "index row requires ... bytes, maximum size is 8191"
Sintoma: Inserções em partições com colunasJSONBouTEXTmuito grandes falham com erro de tamanho de índice.
Causa: Embora não seja exclusivo de tabelas particionadas, o problema é amplificado quando índicesGINouGiSTsão criados em dados não estruturados muito grandes. O limite de 8191 bytes por entrada de índice B-tree (ou 1/3 da página de 8KB para GiST/GIN) pode ser atingido.
Solução: Revise os índices — você realmente precisa indexar o campoJSONBinteiro? Considere criar índices expressionais apenas nos campos relevantes:CREATE INDEX idx_detalhes_ip ON logs_autenticacao ((detalhes->>'ip'));. Para dados muito longos, utilize índicesGINcom a opçãojsonb_path_opsoupg_trgmconforme o caso.
Boas Práticas e Dicas Avançadas
Após implementar dezenas de projetos de particionamento em ambientes que vão de startups a grandes corporações, consolidamos um conjunto de recomendações que vão além da documentação oficial. A primeira delas é sobre naming convention: adote um padrão consistente e previsível — como {tabela}_{ano}_{mes} para séries temporais ou {tabela}_{categoria} para partições LIST. Isso facilita automação, monitoramento e debugging. Evite caracteres especiais e mantenha os nomes em minúsculas (o PostgreSQL é case-sensitive quando aspas são usadas, e nomes sem aspas são convertidos para minúsculas automaticamente).
A segunda prática essencial diz respeito ao planejamento de capacidade. Determine antecipadamente quantas partições seu sistema suportará. Embora o PostgreSQL lide bem com milhares de partições, há um ponto de inflexão — em nossos testes na JRT Technology Solutions, observamos degradação no tempo de planejamento de consultas (planning time) a partir de aproximadamente 5.000 partições para consultas sem pruning efetivo. Considere implementar particionamento em dois níveis (subparticionamento) para cenários extremos: por exemplo, particionar por ano (RANGE) e, dentro de cada ano, por região (LIST).
Terceiro: implemente monitoramento proativo das partições. Crie uma tabela de metadados que registre a última vez que cada partição recebeu dados, seu tamanho em disco e o número de tuplas estimadas. Um script simples pode alertar quando uma partição default receber registros (indicando necessidade de criar nova partição) ou quando partições antigas não são mais acessadas (candidatas a arquivamento).
| Critério | RANGE | LIST | HASH |
|---|---|---|---|
| Versão mínima do PostgreSQL | 10 | 10 | 11 |
| Coluna ideal |
Quer aprender na prática com especialistas?
A JRT Technology Solutions oferece treinamentos e implementação de PostgreSQL para equipes corporativas.