PostgreSQL 17 e 18: Arquitetura de Otimizações de Agregação

Entenda como PostgreSQL otimiza queries com GROUP BY nas versões 17 e 18: hash vs sort aggregation, paralelização, melhorias em partitioned tables e estratégias práticas.

Evolução das Otimizações de Agregação no PostgreSQL: Da Versão 17 às Melhorias em Desenvolvimento

Queries com GROUP BY e funções de agregação costumam ser os principais gargalos em aplicações de analytics e reporting. Um dashboard que consolida vendas por região, uma query que calcula métricas diárias de uso, um relatório financeiro que soma transações por categoria — todos dependem de operações de agregação eficientes. Conforme o volume de dados cresce, essas queries podem facilmente passar de milissegundos para minutos, travando pipelines inteiros de processamento.

O PostgreSQL tem trabalhado progressivamente nessas otimizações. A versão 17 (lançada em setembro de 2024) trouxe melhorias importantes na forma como o banco processa agregações. Mais interessante ainda: discussões técnicas ativas na comunidade revelam que a versão 18, prevista para 2025, está recebendo patches focados especificamente em reduzir consumo de memória e melhorar paralelização em operações de hash aggregation.

Entender a arquitetura por trás dessas otimizações não é apenas curiosidade técnica — é o que permite reescrever queries existentes para aproveitar melhor os recursos do banco e identificar quando um upgrade de versão vai realmente impactar sua aplicação.

A Arquitetura de Agregações: Como o PostgreSQL Processa GROUP BY

Quando você executa uma query com GROUP BY, o PostgreSQL precisa escolher entre duas estratégias fundamentais: sort-based aggregation ou hash-based aggregation. Essa decisão acontece no query planner e impacta diretamente memory usage, CPU e tempo de resposta.

Na abordagem sort-based, o banco ordena os dados pelas colunas de agrupamento antes de calcular as agregações. Funciona bem quando há índices disponíveis ou quando o resultado precisa estar ordenado de qualquer forma. O custo de memória é previsível porque o sort pode ser feito em disco se necessário — o PostgreSQL usa work_mem para controlar quanto de RAM pode usar antes de spill to disk.

Hash aggregation constrói uma hash table em memória com as chaves de agrupamento e acumula os valores agregados diretamente. É mais rápido quando cabe em memória, mas surge um problema com high cardinality — muitos grupos distintos. Se a hash table explodir o work_mem disponível, o PostgreSQL precisa fazer batching ou fallback para sort, e aí o desempenho despenca.

O planner escolhe entre essas estratégias baseado em estatísticas da tabela, estimativas de cardinalidade e configurações de memória. Você consegue ver exatamente o que foi decidido usando EXPLAIN ANALYZE, onde aparecem nodes como ‘HashAggregate’ ou ‘GroupAggregate’ (a variante sort-based).

Parallel Aggregation: O Modelo de Partial + Finalize

A paralelização de agregações existe desde a versão 9.6, mas o modelo evoluiu significativamente. O conceito fundamental é dividir o trabalho entre múltiplos processos workers, cada um processando um subset dos dados em paralelo, e depois combinar os resultados parciais.

O PostgreSQL implementa isso através de um padrão Partial Aggregate + Finalize Aggregate. Cada worker executa a fase Partial, calculando agregações intermediárias sobre sua porção dos dados. Depois, um processo coordenador ou um único worker executa a fase Finalize, combinando os resultados parciais no resultado final. No execution plan, isso aparece como dois nodes distintos.

Essa arquitetura funciona bem para funções agregadas standard como SUM, COUNT, AVG — elas são “combinable” naturalmente. Você pode somar somas parciais, contar contagens parciais. Para agregações mais complexas como percentis, mediana ou agregações custom, o PostgreSQL precisa saber como combinar resultados parciais, e nem sempre isso é possível ou eficiente.

A partir da versão 10, o PostgreSQL consegue fazer parallel hash aggregate, onde múltiplos workers constroem hash tables parciais simultaneamente. Isso escala bem para queries com muitos grupos, desde que o total de memória usada pelos workers (somados) não ultrapasse limites configurados. O trade-off aqui é entre paralelização (mais workers = mais rápido) e memory pressure (mais workers = mais hash tables em memória).

Melhorias Concretas no PostgreSQL 17

A versão 17 focou em áreas específicas que impactam queries de agregação, embora não tenha trazido uma revolução arquitetural. Uma melhoria significativa está relacionada a aggregate pushdown em partitioned tables — o banco agora consegue empurrar operações de agregação para partições individuais com mais inteligência, processando cada partição independentemente antes de combinar resultados.

Outra área de progresso foi em RIGHT e OUTER joins que alimentam agregações. A documentação oficial indica otimizações nesses tipos de join, o que beneficia queries analíticas comuns onde você precisa agregar dados de múltiplas tabelas, incluindo casos onde algumas entidades não têm correspondência (típico em relatórios que mostram zeros para categorias sem dados).

O suporte a MERGE com RETURNING também foi introduzido na versão 17. Embora não seja diretamente uma otimização de agregação, permite patterns mais eficientes onde você precisa fazer upserts em massa e agregar resultados da operação — casos comuns em ETL e pipelines de processamento.

A documentação oficial não especifica ganhos percentuais de performance ou fornece benchmarks quantitativos comparando a versão 17 com anteriores especificamente para operações de GROUP BY. O que temos são as release notes indicando as melhorias implementadas, mas sem métricas públicas de impacto.

O Que Está Sendo Desenvolvido para PostgreSQL 18

As discussões técnicas na mailing list pgsql-hackers revelam onde a comunidade está direcionando esforços para a versão 18, prevista para 2025. Um tema recorrente é “incremental sort for aggregates” — permitir que o PostgreSQL aproveite ordenações parciais já existentes (via índices ou operações anteriores) para executar agregações de forma mais eficiente.

Se seus dados estão 80% ordenados pelas colunas de GROUP BY mas não completamente, o PostgreSQL atualmente trata isso como não-ordenado e pode escolher hash aggregation. Com incremental sort aplicado a agregações, o banco poderia processar chunks ordenados, fazer agregações parciais nesses chunks, e depois combinar — economizando tanto sort operations quanto memory overhead.

Outro foco importante são patches propostos para redução de memory usage em hash aggregations com high cardinality. O problema clássico: você tem uma query agrupando por user_id onde existem milhões de usuários distintos. A hash table cresce demais, ultrapassa work_mem, e o PostgreSQL precisa fazer batching com spill to disk. Os patches em discussão propõem algoritmos mais inteligentes para detectar essa situação antecipadamente e escolher estratégias híbridas.

Há também debate técnico sobre melhorias em parallel hash aggregate especificamente para arquiteturas NUMA (Non-Uniform Memory Access), comuns em servidores modernos. O desafio é que, em sistemas NUMA, acessar memória de outro nó NUMA é mais lento que acessar memória local. Otimizar como os workers alocam e compartilham hash tables pode trazer ganhos significativos nesses ambientes.

Esses são patches em discussão e revisão. O status atual, segundo o repositório oficial, indica que ainda não foram merged. Nem todos chegarão na versão 18, e a implementação final pode mudar substancialmente em relação às propostas iniciais.

Limitações Práticas e Trade-offs Conhecidos

Mesmo com todas as otimizações, existem limitações arquiteturais fundamentais que qualquer query de agregação enfrenta. Aggregate pushdown, por exemplo, não funciona quando você usa DISTINCT ou ORDER BY dentro da função agregada em todas as situações. O PostgreSQL precisa ver todos os dados antes de aplicar essas operações, o que impede paralelização ou pushdown para partições.

O trade-off entre hash e sort aggregation não tem resposta única. Hash é mais rápido quando cabe em memória, mas imprevisível em memory usage — depende da cardinalidade real dos grupos, que o planner só consegue estimar. Sort tem overhead de CPU maior para ordenar, mas memory usage é controlável e previsível porque pode fazer spill to disk incrementalmente.

Para queries paralelas, aumentar o número de workers nem sempre ajuda. Cada worker adicional consome work_mem, e você pode facilmente estourar RAM total do sistema. Há overhead de coordenação entre workers — em queries rápidas (milissegundos), o custo de setup de paralelização pode ser maior que o ganho.

A paralelização incremental de sorts, introduzida na versão 16, ajuda em queries onde você precisa ordenar e agregar. A documentação não detalha especificamente quais cenários se beneficiam mais ou quais limitações ainda existem. Na prática, você precisa testar com EXPLAIN ANALYZE nos seus dados reais.

Aproveitando Otimizações em Queries Existentes

Entender a arquitetura permite abordar otimização de forma metódica. Use EXPLAIN ANALYZE para ver o execution plan real. Se você vê HashAggregate com muitos workers mas a query ainda é lenta, provavelmente está batendo em memory limits. Aumentar work_mem pode ajudar, mas cuidado: esse parâmetro é per-operation per-worker, então se você tem 4 workers e 3 hash aggregates no plan, você está consumindo 12x o work_mem configurado.

Se o plan mostra GroupAggregate em vez de HashAggregate, o planner estimou que a cardinalidade é alta demais para hash. Você pode forçar hash com SET enable_sort = off, mas geralmente o planner está certo — o que você precisa é melhorar os índices para aproveitar ordenação existente ou particionar a tabela de forma que reduz cardinalidade por partition.

Para queries em partitioned tables, confirme que aggregate pushdown está acontecendo. Se não estiver, pode ser por causa de DISTINCT, ORDER BY, ou filtros que o planner não consegue empurrar para as partições. Às vezes, reescrever a query para fazer agregação em duas etapas (uma por partição, outra combinando) pode ser mais rápido.

Monitore o balance entre parallel workers e memória disponível. Se você vê muitos ‘Partial Aggregate’ nodes mas o tempo total não melhora proporcionalmente, pode estar limitado por memory bandwidth ou contention na fase Finalize. Reduzir workers ou aumentar work_mem pode paradoxalmente melhorar performance.

Quando planejar upgrade de versão, teste suas queries mais críticas no novo PostgreSQL antes de migrar produção. As otimizações em RIGHT/OUTER joins da versão 17 podem mudar execution plans significativamente, e nem sempre para melhor dependendo da estrutura dos seus dados e queries. Não existe garantia universal de ganho de performance — você precisa validar no seu workload específico.


← Voltar para home