PostgreSQL: Tabelas Particionadas e Herança de Tabelas no…
Última atualização: 2026-08-26
1. O Que Você Vai Aprender
- Entender o princípio e os casos de uso do particionamento declarativo
- Criar partições RANGE / LIST / HASH / multinível
- Usar poda de partições para acelerar consultas
- Realizar manutenção de partições: DETACH / ATTACH / DROP
- Comparar herança de tabelas (INHERITS) com particionamento declarativo
- Projetar uma estratégia de indexação para tabelas particionadas
2. A História
A tabela orders da plataforma de e-commerce de Alice ganha 100 mil novas linhas por dia. Após três anos, a tabela única ultrapassa 100 milhões de linhas; mesmo com índices, uma consulta mensal ainda varre a tabela inteira. O DBA sugere particionamento RANGE mensal — consultar os últimos 30 dias então varre apenas 1 partição em vez de 3 anos da tabela inteira. Após o particionamento entrar em produção, a consulta do relatório mensal caiu de 12 segundos para 0,8 segundos e a E/S de disco caiu 95%.
3. Conceito: Visão Geral do Particionamento Declarativo
(1) Por Que o Particionamento é Necessário
Quando uma tabela única ultrapassa dezenas de milhões de linhas, a B-Tree do índice fica mais profunda, o VACUUM leva muito mais tempo e o planejador de consultas pode escolher um plano subótimo. O particionamento divide uma tabela lógica grande em várias tabelas físicas pequenas, cada uma com seu próprio índice independente e manutenção.
| Métrica | Não particionada (100M linhas) | Partições mensais (36 partições) |
|---|---|---|
| Linhas por partição | 100M | ~2,8M |
| Profundidade do índice | 5-6 níveis | 3-4 níveis |
| Tempo de VACUUM | 30+ min | < 2 min/partição |
| E/S de consulta mensal | Varredura completa da tabela | Varre apenas 1 partição |
(2) Evolução do Particionamento no PostgreSQL
| Versão | Recurso |
|---|---|
| PG 9.x | Herança de tabelas + particionamento manual baseado em gatilhos |
| PG 10 | Particionamento declarativo RANGE / LIST |
| PG 11 | Particionamento HASH, poda de partições aprimorada, migração de UPDATE entre partições |
| PG 12 | Partições ATTACH/DETACH |
| PG 13 | Otimização de poda de partições multinível |
| PG 14+ | Melhorias adicionais de desempenho na poda de partições |
(3) Quatro Estratégias de Particionamento
| Estratégia | Caso de uso | Requisito da chave de partição |
|---|---|---|
| RANGE | Séries temporais, intervalos numéricos | Tipo ordenável |
| LIST | Categorias de enumeração (região, status) | Valores discretos |
| HASH | Distribuição uniforme, sem intervalo claro | Qualquer tipo hashável |
| MULTINÍVEL | Combinação RANGE + LIST/HASH | Combinação de múltiplas colunas |
4. Operação: Particionamento RANGE
▶ Exemplo: Criar uma Tabela de Pedidos Particionada Mensalmente por RANGE
-- Tabela pai: apenas chave de partição
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY,
user_id BIGINT NOT NULL,
order_date DATE NOT NULL,
total_amount NUMERIC(12,2),
status TEXT DEFAULT 'pending'
) PARTITION BY RANGE (order_date);
-- Partições mensais para 2024
CREATE TABLE orders_2024_01 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE orders_2024_02 PARTITION OF orders
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
CREATE TABLE orders_2024_03 PARTITION OF orders
FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');
-- Partição padrão captura linhas fora do intervalo
CREATE TABLE orders_default PARTITION OF orders DEFAULT;
Output:
CREATE TABLE
▶ Exemplo: Função para Gerar Partições Automaticamente
-- Gera partições mensais para um determinado ano
CREATE OR REPLACE FUNCTION create_monthly_partitions(
p_parent REGCLASS,
p_year INT
) RETURNS VOID AS $$
DECLARE
m INT;
p_name TEXT;
s_date TEXT;
e_date TEXT;
BEGIN
FOR m IN 1..12 LOOP
p_name := format('%s_%s_%02s', p_parent::text, p_year, m);
s_date := format('%s-%02s-01', p_year, m);
e_date := format('%s-%02s-01', p_year, m + 1);
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF %s
FOR VALUES FROM (%L) TO (%L)',
p_name, p_parent::text, s_date, e_date
);
END LOOP;
END;
$$ LANGUAGE plpgsql;
SELECT create_monthly_partitions('orders', 2025);
Output:
CREATE TABLE
▶ Exemplo: Inserção e Consulta em Partição RANGE
INSERT INTO orders (user_id, order_date, total_amount, status)
VALUES
(1001, '2024-01-15', 299.99, 'completed'),
(1002, '2024-02-20', 159.50, 'shipped'),
(1003, '2024-03-10', 89.00, 'pending');
-- Consulta com poda de partições
SELECT * FROM orders
WHERE order_date BETWEEN '2024-02-01' AND '2024-02-29';
id | user_id | order_date | total_amount | status
------+---------+------------+--------------+--------
1002 | 1002 | 2024-02-20 | 159.50 | shipped
(1 row)
5. Operação: Particionamento LIST e HASH
▶ Exemplo: Partição LIST — Dividir Tabela de Usuários por Região
CREATE TABLE users_by_region (
id BIGINT GENERATED ALWAYS AS IDENTITY,
name TEXT NOT NULL,
region TEXT NOT NULL,
email TEXT
) PARTITION BY LIST (region);
CREATE TABLE users_north_america PARTITION OF users_by_region
FOR VALUES IN ('US', 'CA', 'MX');
CREATE TABLE users_europe PARTITION OF users_by_region
FOR VALUES IN ('UK', 'DE', 'FR', 'ES');
CREATE TABLE users_asia PARTITION OF users_by_region
FOR VALUES IN ('CN', 'JP', 'KR', 'SG');
CREATE TABLE users_other PARTITION OF users_by_region DEFAULT;
Output:
CREATE TABLE
▶ Exemplo: Particionamento HASH — Distribuir Uniformemente uma Tabela de Log
CREATE TABLE access_logs (
id BIGINT GENERATED ALWAYS AS IDENTITY,
user_id BIGINT,
action TEXT,
log_time TIMESTAMPTZ DEFAULT now()
) PARTITION BY HASH (user_id);
-- Cria 8 partições hash
CREATE TABLE access_logs_p0 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 0);
CREATE TABLE access_logs_p1 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 1);
CREATE TABLE access_logs_p2 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 2);
CREATE TABLE access_logs_p3 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 3);
CREATE TABLE access_logs_p4 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 4);
CREATE TABLE access_logs_p5 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 5);
CREATE TABLE access_logs_p6 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 6);
CREATE TABLE access_logs_p7 PARTITION OF access_logs
FOR VALUES WITH (MODULUS 8, REMAINDER 7);
Output:
CREATE TABLE
▶ Exemplo: Particionamento Multinível (RANGE + LIST)
CREATE TABLE order_details (
id BIGINT,
order_id BIGINT,
order_date DATE NOT NULL,
region TEXT NOT NULL,
product_id INT,
quantity INT
) PARTITION BY RANGE (order_date);
-- Primeiro nível: por mês
CREATE TABLE order_details_2024q1 PARTITION OF order_details
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01')
PARTITION BY LIST (region);
-- Segundo nível: por região dentro de cada intervalo mensal
CREATE TABLE order_details_2024q1_na PARTITION OF order_details_2024q1
FOR VALUES IN ('US', 'CA', 'MX');
CREATE TABLE order_details_2024q1_eu PARTITION OF order_details_2024q1
FOR VALUES IN ('UK', 'DE', 'FR');
Output:
CREATE TABLE
6. Conceito: Poda de Partições
(1) Princípio da Poda
A poda de partições permite que o planejador de consultas descarte partições irrelevantes no momento do planejamento, evitando varreduras em tempo de execução.
flowchart TD
A["SELECT * FROM orders<br/>WHERE order_date = '2024-02-15'"] --> B["Planejador de Consultas"]
B --> C{"Poda de Partições"}
C -->|"order_date em [2024-02-01, 2024-03-01)"| D["orders_2024_02 ✓"]
C -->|"fora do intervalo"| E["orders_2024_01 ✗"]
C -->|"fora do intervalo"| F["orders_2024_03 ✗"]
C -->|"fora do intervalo"| G["orders_default ✗"]
D --> H["Varre apenas 1 partição"]
(2) Verificar Efeito da Poda
-- Habilita poda de partições (padrão ON)
SET enable_partition_pruning = on;
-- Verifica quais partições são varridas
EXPLAIN (COSTS OFF) SELECT * FROM orders
WHERE order_date = '2024-02-15';
Append
-> Seq Scan on orders_2024_02
Filter: (order_date = '2024-02-15'::date)
(3) Causas Comuns de Falha na Poda
| Cenário | Podada? | Motivo |
|---|---|---|
WHERE order_date = '2024-02-15' |
✅ | Constante é podável |
WHERE order_date = $1 (stmt preparado) |
✅ PG 11+ | Poda de parâmetro genérico |
WHERE order_date = now() |
✅ | Função estável é podável |
WHERE order_date = random_func() |
❌ | Função volátil não é podável |
WHERE to_char(order_date, 'YYYY-MM') = '2024-02' |
❌ | Função envolve a chave de partição |
7. Operação: Manutenção de Partições
▶ Exemplo: DETACH de Partição Antiga para Arquivamento
-- Desanexa partição de janeiro de 2023 (sem perda de dados)
ALTER TABLE orders DETACH PARTITION orders_2023_01;
-- Agora é uma tabela independente, pode ser movida para armazenamento mais barato
ALTER TABLE orders_2023_01 SET TABLESPACE archive_tbs;
-- Ou exporta e remove
COPY orders_2023_01 TO '/archive/orders_2023_01.csv';
DROP TABLE orders_2023_01;
Output:
-- Comando SQL executado com sucesso
▶ Exemplo: ATTACH de uma Nova Partição
-- Cria nova tabela primeiro
CREATE TABLE orders_2025_01 (LIKE orders INCLUDING DEFAULTS);
-- Adiciona restrição check para validação (acelera ATTACH)
ALTER TABLE orders_2025_01
ADD CONSTRAINT orders_2025_01_check
CHECK (order_date >= '2025-01-01' AND order_date < '2025-02-01');
-- Anexa à tabela pai (obtém bloqueio exclusivo brevemente)
ALTER TABLE orders ATTACH PARTITION orders_2025_01
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
Output:
CREATE TABLE
▶ Exemplo: DETACH Concorrente (PG 14+)
-- DETACH sem bloquear leituras concorrentes
ALTER TABLE orders DETACH PARTITION orders_2023_01 CONCURRENTLY;
Output:
-- Comando SQL executado com sucesso
▶ Exemplo: Estratégia de Indexação de Partições
-- Índice na tabela pai se propaga para todas as partições
CREATE INDEX idx_orders_user_id ON orders (user_id);
-- Cada partição obtém seu próprio índice
\d orders_2024_01
Indexes:
"orders_2024_01_user_id_idx" btree (user_id)
| Estratégia de indexação | Descrição |
|---|---|
| Criar índice na tabela pai | Propaga automaticamente para todas as partições existentes e futuras |
| Criar índice em uma única partição | Afeta apenas essa partição; deve sincronizar manualmente no ATTACH |
| Índice único | Deve incluir a chave de partição (a unicidade entre partições é garantida pela chave de partição) |
▶ Exemplo: Índice Único Deve Incluir a Chave de Partição
-- Isso FALHA: único sem chave de partição
CREATE UNIQUE INDEX idx_orders_id ON orders (id);
-- ERROR: unique constraint must contain partition key
-- Isso FUNCIONA: único com chave de partição
CREATE UNIQUE INDEX idx_orders_id_date ON orders (id, order_date);
Output:
CREATE TABLE
8. Conceito: Herança de Tabelas (INHERITS)
(1) Sintaxe e Características da Herança
A herança de tabelas é um recurso exclusivo do PostgreSQL, mais flexível que o particionamento declarativo — as tabelas filhas podem ter colunas extras e não precisam cobrir todos os domínios de valor da tabela pai.
-- Tabela pai
CREATE TABLE people (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT
);
-- Tabela filha herda todas as colunas + adiciona extras
CREATE TABLE employees (
salary NUMERIC(10,2),
dept TEXT,
hire_date DATE
) INHERITS (people);
-- Adiciona chave primária à filha
ALTER TABLE employees ADD PRIMARY KEY (id);
(2) Herança vs Particionamento Declarativo
| Recurso | Herança de tabelas (INHERITS) | Particionamento declarativo |
|---|---|---|
| Colunas extras na filha | ✅ Permitido | ❌ Deve compartilhar a mesma estrutura |
| Tabela pai armazena dados | ✅ Sim | ❌ Tabela pai é uma casca vazia |
| Roteamento automático de INSERT | ❌ Precisa de um gatilho | ✅ Automático |
| Poda de partições | ❌ Precisa de restrições CHECK | ✅ Automático |
| Restrição única entre tabelas | ❌ Apenas tabela única | ✅ Com chave de partição, entre tabelas |
| Chave estrangeira para a tabela pai | ❌ Não suportado | ✅ Suportado |
| Flexibilidade | Alta | Média |
| Cenário recomendado | Subtipos heterogêneos | Otimização de divisão de tabelas grandes |
(3) Consulta de Herança: a Palavra-chave ONLY
-- Consulta pai + todas as filhas
SELECT * FROM people;
-- Consulta apenas o pai (sem filhas)
SELECT * FROM ONLY people;
-- Verifica de qual tabela cada linha vem
SELECT tableoid::regclass, * FROM people;
9. Operação: Otimização de Consultas Entre Partições
▶ Exemplo: Coletar Estatísticas das Partições
-- Analisa partição específica
ANALYZE orders_2024_02;
-- Analisa todas as partições via tabela pai
ANALYZE orders;
-- Verifica estatísticas das partições
SELECT relname, n_live_tup, last_analyze
FROM pg_stat_user_tables
WHERE relname LIKE 'orders_%'
ORDER BY relname;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: Otimização de JOIN Entre Partições
-- Junção por partição (PG 12+)
SET enable_partitionwise_join = on;
SELECT o.id, o.total_amount, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.order_date BETWEEN '2024-01-01' AND '2024-03-31';
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| Parâmetro | Padrão | Descrição |
|---|---|---|
enable_partition_pruning |
on | Poda de partições |
enable_partitionwise_join |
off | JOIN no nível da partição |
enable_partitionwise_aggregate |
off | Agregação no nível da partição |
constraint_exclusion |
partition | Exclusão de restrição (para herança) |
▶ Exemplo: Cenário Híbrido de Partição + Herança
-- Tabela base de auditoria
CREATE TABLE audit_log (
id BIGINT GENERATED ALWAYS AS IDENTITY,
table_name TEXT NOT NULL,
action TEXT NOT NULL,
changed_at TIMESTAMPTZ DEFAULT now()
) PARTITION BY RANGE (changed_at);
-- Partições mensais, cada uma herdada por tabelas específicas de tipo
CREATE TABLE audit_log_2024_01 PARTITION OF audit_log
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
-- Colunas adicionais para tipo de auditoria específico via herança
CREATE TABLE audit_log_user_changes (
old_email TEXT,
new_email TEXT
) INHERITS (audit_log_2024_01);
Output:
CREATE TABLE
10. Exemplo Abrangente
-- Configuração completa de particionamento mensal para pedidos de e-commerce
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY,
user_id BIGINT NOT NULL,
order_date DATE NOT NULL,
total_amount NUMERIC(12,2) DEFAULT 0,
status TEXT DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT now()
) PARTITION BY RANGE (order_date);
-- Cria partições para o primeiro trimestre de 2024
CREATE TABLE orders_2024_01 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE orders_2024_02 PARTITION OF orders
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
CREATE TABLE orders_2024_03 PARTITION OF orders
FOR VALUES FROM ('2024-03-01') TO ('2024-04-01');
CREATE TABLE orders_default PARTITION OF orders DEFAULT;
-- Índice único deve incluir chave de partição
CREATE UNIQUE INDEX idx_orders_id_date ON orders (id, order_date);
CREATE INDEX idx_orders_user_date ON orders (user_id, order_date);
-- Insere dados de teste
INSERT INTO orders (user_id, order_date, total_amount, status) VALUES
(1001, '2024-01-05', 299.99, 'completed'),
(1001, '2024-02-14', 159.50, 'shipped'),
(1002, '2024-01-20', 450.00, 'completed'),
(1002, '2024-03-01', 89.00, 'pending'),
(1003, '2024-02-28', 1200.00,'completed');
-- Verifica poda de partições
EXPLAIN (COSTS OFF) SELECT * FROM orders
WHERE order_date BETWEEN '2024-02-01' AND '2024-02-28';
-- Desanexa partição antiga para arquivamento
ALTER TABLE orders DETACH PARTITION orders_2024_01;
COPY orders_2024_01 TO '/archive/orders_2024_01.csv';
-- Anexa nova partição para o próximo trimestre
CREATE TABLE orders_2024_04 (LIKE orders INCLUDING DEFAULTS);
ALTER TABLE orders_2024_04
ADD CONSTRAINT chk_2024_04
CHECK (order_date >= '2024-04-01' AND order_date < '2024-05-01');
ALTER TABLE orders ATTACH PARTITION orders_2024_04
FOR VALUES FROM ('2024-04-01') TO ('2024-05-01');
-- Monitora tamanhos das partições
SELECT relname,
pg_size_pretty(pg_total_relation_size(oid)) AS size,
reltuples::bigint AS row_estimate
FROM pg_class
WHERE relname LIKE 'orders_2024%' ORDER BY relname;
❓ Perguntas Frequentes
P: A chave primária de uma tabela particionada deve incluir a chave de partição? R: Sim. O PostgreSQL exige que a restrição única de uma tabela particionada (incluindo a chave primária) inclua a coluna da chave de partição, caso contrário a unicidade entre partições não pode ser garantida.
P: Uma tabela simples existente pode ser convertida em uma tabela particionada? R: Não diretamente. Você deve criar uma nova tabela pai particionada e depois migrar os dados com INSERT INTO...SELECT ou ATTACH.
P: Qual é o risco de uma partição DEFAULT? R: Uma partição DEFAULT recebe todos os dados que não correspondem a nada mais; se o intervalo de uma nova partição se sobrepuser a dados já na DEFAULT, anexar a nova partição falha. Revise periodicamente o conteúdo da partição DEFAULT.
P: Existe um limite superior para o número de partições? R: O PostgreSQL não tem limite fixo, mas cada partição adiciona sobrecarga ao planejador. Mantenha a contagem de partições de uma única tabela abaixo de algumas centenas; acima de 1.000 partições, o tempo de planejamento pode aumentar notavelmente.
P: A herança de tabelas pode substituir o particionamento declarativo? R: Geralmente não é recomendado. A herança carece de roteamento automático, poda e restrições entre tabelas, e é cara de manter. Use herança apenas quando precisar de tabelas filhas heterogêneas (colunas extras).
P: O particionamento HASH pode usar poda de partições? R: Sim. Desde que a condição WHERE inclua uma comparação de igualdade na chave de partição, o PG pode calcular em qual bucket hash os dados caem e podar as outras partições.
P: Um UPDATE que move uma linha entre partições bloqueia a tabela? R: PG 11+ suporta UPDATE entre partições; internamente é um DELETE + INSERT, que não bloqueia a tabela inteira, mas obtém bloqueios de linha em cada uma das duas partições.
📖 Resumo
- Particionamento declarativo (RANGE/LIST/HASH) é a maneira padrão de gerenciar tabelas grandes no PG 10+
- Particionamento RANGE é adequado para séries temporais; LIST para categorias de enumeração; HASH para distribuição uniforme
- A poda de partições faz com que uma consulta varra apenas as partições relevantes; a condição deve estar diretamente na chave de partição
- DETACH/ATTACH permitem manutenção online de partições; CONCURRENTLY reduz o bloqueio
- O índice único de uma tabela particionada deve incluir a chave de partição
- A herança de tabelas é mais flexível, mas carece de roteamento automático e poda; adequada para cenários de subtipos heterogêneos
enable_partitionwise_join/aggregatepode melhorar ainda mais o desempenho de consultas entre partições
📝 Exercícios
-
⭐ Crie uma tabela
paymentsparticionada por trimestre com RANGE, com partições para os quatro trimestres de 2024, e verifique o efeito de poda paraWHERE payment_date BETWEEN '2024-Q2'. -
⭐⭐ Projete um esquema de particionamento LIST para um sistema SaaS multi-inquilino: distribua
tenant_idpor hash em 4 partições, escreva as instruções de criação e teste o isolamento de dados entre diferentes inquilinos. -
⭐⭐⭐ Escreva um procedimento armazenado que receba um ano como entrada, crie automaticamente 12 partições mensais para a tabela
orders, adicione restrições CHECK, crie índices e faça DETACH da partição de janeiro do ano anterior para um tablespace de arquivamento. Teste o fluxo completo de INSERT, consulta entre partições e verificação de poda.