PostgreSQL: Tabelas Particionadas e Herança de Tabelas no…

Última atualização: 2026-08-26

1. O Que Você Vai Aprender


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

SQL
-- 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:

TEXT 📖 Somente leitura
CREATE TABLE

▶ Exemplo: Função para Gerar Partições Automaticamente

SQL
-- 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:

TEXT 📖 Somente leitura
CREATE TABLE

▶ Exemplo: Inserção e Consulta em Partição RANGE

SQL
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';
TEXT 📖 Somente leitura
  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

SQL
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:

TEXT 📖 Somente leitura
CREATE TABLE

▶ Exemplo: Particionamento HASH — Distribuir Uniformemente uma Tabela de Log

SQL
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:

TEXT 📖 Somente leitura
CREATE TABLE

▶ Exemplo: Particionamento Multinível (RANGE + LIST)

SQL
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:

TEXT 📖 Somente leitura
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.

100%
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

SQL
-- 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';
TEXT 📖 Somente leitura
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

SQL
-- 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:

TEXT 📖 Somente leitura
-- Comando SQL executado com sucesso

▶ Exemplo: ATTACH de uma Nova Partição

SQL
-- 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:

TEXT 📖 Somente leitura
CREATE TABLE

▶ Exemplo: DETACH Concorrente (PG 14+)

SQL
-- DETACH sem bloquear leituras concorrentes
ALTER TABLE orders DETACH PARTITION orders_2023_01 CONCURRENTLY;

Output:

TEXT 📖 Somente leitura
-- Comando SQL executado com sucesso

▶ Exemplo: Estratégia de Indexação de Partições

SQL
-- Í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
TEXT 📖 Somente leitura
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

SQL
-- 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:

TEXT 📖 Somente leitura
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.

SQL
-- 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

SQL
-- 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

SQL
-- 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:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Exemplo: Otimização de JOIN Entre Partições

SQL
-- 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:

TEXT 📖 Somente leitura
 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

SQL
-- 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:

TEXT 📖 Somente leitura
CREATE TABLE

10. Exemplo Abrangente

SQL
-- 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


📝 Exercícios

  1. ⭐ Crie uma tabela payments particionada por trimestre com RANGE, com partições para os quatro trimestres de 2024, e verifique o efeito de poda para WHERE payment_date BETWEEN '2024-Q2'.

  2. ⭐⭐ Projete um esquema de particionamento LIST para um sistema SaaS multi-inquilino: distribua tenant_id por hash em 4 partições, escreva as instruções de criação e teste o isolamento de dados entre diferentes inquilinos.

  3. ⭐⭐⭐ 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%