Novidades no planejador de consultas do Postgres 16
(citusdata.com)- O PostgreSQL 16 adiciona 10 melhorias ao planejador/otimizador de consultas, ampliando as opções de escolha de planos de execução para consultas com
DISTINCT, agregações, joins, funções de janela e tabelas particionadas - Ele passa a aproveitar de forma mais agressiva entradas parcialmente ordenadas em
SELECT DISTINCT, agregações comORDER BY/DISTINCTe no processamento apósMerge Join, permitindo produzir resultados com menos memória do que uma ordenação completa - O suporte a Memoize dentro de
UNION ALL,Right Anti JoineParallel Hash Joinpara joinsFULL/RIGHTfoca em reduzir consultas repetidas e o custo de criação de grandes tabelas hash - As funções de janela reduzem o processamento desnecessário de
RANGEe deWindowAggque antes precisava ir até o fim, e algumas funções agora podem ser interrompidas antecipadamente conforme a condição - Como todas as melhorias vêm ativadas por padrão, vale a pena comparar
EXPLAINe tempos de execução da carga real antes e depois da atualização para o PostgreSQL 16
Escopo das melhorias do planejador no PostgreSQL 16
- O PostgreSQL 16 introduz várias melhorias no planejador de consultas, permitindo que muitas consultas SQL rodem mais rápido do que em versões anteriores do PostgreSQL
- O texto detalha as melhorias de planejador incluídas nas notas de lançamento do PG16, junto com comparações de saída de
EXPLAINentre PG15 e PG16 e exemplos de teste reproduzíveis - Aqui, o planejador é o componente que em outros bancos de dados relacionais costuma ser chamado de otimizador
Otimizações de ordenação e DISTINCT
-
Uso de Incremental Sort em
SELECT DISTINCT- O Incremental Sort foi adicionado pela primeira vez no PostgreSQL 13 e reduz custo ao ordenar apenas as colunas restantes quando o resultado já vem ordenado pelas colunas iniciais
- O planejador do PostgreSQL 16 também passa a considerar Incremental Sort em consultas
SELECT DISTINCT - Por exemplo, se houver um índice btree na coluna
ae a ordem necessária fora, b, é possível obter do índice um resultado já ordenado porae então ordenar apenasbsempre que o valor deamudar - No quicksort do PostgreSQL, pode ser mais eficiente ordenar vários grupos pequenos do que um único grupo grande
- Na consulta de exemplo, o PG15 usou
HashAggregatee varredura sequencial, enquanto o PG16 escolheu o índicedistinct_test_a_idxeIncremental Sort - Em PG16,
Presorted Key: asignifica que a entrada já ordenada porafoi aproveitada - O método com hash do PG15 despejou cerca de 30MB em disco, enquanto o pico de memória do
Incremental Sortno PG16 foi de 26KB - O tempo de execução caiu de 414.226ms no PG15 para 263.167ms no PG16
-
Otimização de agregações com
ORDER BYouDISTINCT- Até o PostgreSQL 15, funções de agregação com cláusulas
ORDER BYouDISTINCTsempre faziam a ordenação dentro do nóAggregate - O planejador do PostgreSQL 16 pode montar um plano de execução que forneça linhas na ordem correta ao nó
Aggregate, e o executor ignora a ordenação interna se a entrada já estiver ordenada - No exemplo com
COUNT(DISTINCT b), tanto PG15 quanto PG16 usamGroupAggregateeIndex Only Scan, mas a saída do PG15 mostratemp read=4540 written=4560 - Esse I/O de arquivo temporário é resultado da ordenação implícita do PG15 ter sido despejada em disco
- Na saída do PG16, esse I/O temporário não aparece, e o tempo de execução melhora de 302.693ms no PG15 para 115.534ms no PG16, mais de 2x mais rápido
- Até o PostgreSQL 15, funções de agregação com cláusulas
Melhorias em consultas repetidas e planos de join
-
Aplicação de Memoize dentro de
UNION ALL- O nó de plano
Memoizefoi introduzido no PostgreSQL 14 e funciona como uma camada de cache entre umNested Loopparametrizado e sua entrada interna - O planejador do PostgreSQL 16 passa a considerar
Memoizemesmo quando há uma consultaUNION ALLna parte interna de umNested Loopparametrizado - No exemplo, o PG15 executou
Append1 milhão de vezes, enquanto o PG16 colocouMemoizeacima deAppend - O
Memoizedo PG16 registrouHits: 999990,Misses: 10,Memory Usage: 2kB - O número de execuções de
Appendcaiu de 1 milhão no PG15 para 10 no PG16 - O tempo de execução passou de 1926.151ms no PG15 para 282.120ms no PG16, cerca de 6x mais rápido
- O nó de plano
-
Suporte a Right Anti Join
- Em
Hash JoincomINNER JOIN, normalmente é vantajoso construir a tabela hash sobre a tabela menor - Uma tabela hash menor exige menos trabalho para ser criada, é mais amigável ao cache da CPU e reduz a chance de stalls enquanto a CPU espera dados da memória principal
- Antes do PostgreSQL 16,
Anti Joincolocava sempre na parte interna do join a tabela mencionada emNOT EXISTS, o que podia forçar a criação da tabela hash sobre a tabela maior - O PostgreSQL 16 adiciona suporte a Right Anti Join, permitindo aplicar hash no menor dos dois lados
- No exemplo, o PG15 aplicou hash na tabela
largecom 1 milhão de linhas e usou 6446KB de memória, enquanto o PG16 aplicou hash na tabelasmallcom 100 linhas e usou apenas 12KB - O tempo de execução caiu de 139.023ms no PG15 para 77.076ms no PG16, quase pela metade
- Em
-
Parallel Hash Join para joins FULL/RIGHT
- O PostgreSQL 11 introduziu
Parallel Hash Join, no qual vários workers paralelos participam da criação de uma única tabela hash - No PostgreSQL 16,
Parallel Hash Joinpassa a oferecer suporte aos tipos de joinFULLeRIGHT - Isso permite executar em paralelo também planos de
FULL OUTER JOINeRight Join - No exemplo de
FULL JOIN, o PG15 usou um únicoHash Full Join, enquanto o PG16 usouParallel Hash Full JoineGather - Na saída do PG16 aparecem
Workers Planned: 1eWorkers Launched: 1 - O tempo de execução caiu bastante, de 220.677ms no PG15 para 129.769ms no PG16
- O PostgreSQL 11 introduziu
Otimizações de funções de janela
-
Eliminação de processamento desnecessário de RANGE
- Em funções de janela como
row_number(),rank(),dense_rank(),percent_rank(),cume_dist()entile(), se a cláusula de janela não especificaROWS, o PostgreSQL usa a opção padrãoRANGE - A opção
RANGEprecisa olhar linhas anteriores para encontrar peer rows com o mesmo valor de ordenação, e isso pode ficar caro quando há muitos valores iguais noORDER BY - Para essas funções, o comportamento não muda entre
ROWSeRANGE, mas antes do PostgreSQL 16 o executor não conseguia distinguir isso e precisava verificar peer rows em todos os casos - O planejador do PostgreSQL 16 sabe quais funções de janela são afetadas por
ROWS/RANGEe repassa essa informação ao executor para que ele pule o processamento desnecessário - No exemplo
row_number() <= 10, o PG15 leu 50.410 linhas do índice antes de parar, enquanto o PG16 leu apenas 11 linhas - O PG16 aproveita o fato de que, ao chegar em
row_number11, não pode mais haver linhas que satisfaçam a condição<= 10 - O tempo de execução caiu de 29.775ms no PG15 para 0.058ms no PG16, mais de 500x mais rápido
- Em funções de janela como
-
Ampliação da interrupção antecipada para funções de janela monotônicas
- O PostgreSQL 15 passou a permitir encerrar cedo a execução de
WindowAggquando uma condição na cláusulaWHEREse torna falsa para determinada função de janela e não pode voltar a ser verdadeira - O PostgreSQL 16 amplia essa otimização também para
ntile(),cume_dist()epercent_rank() - No PostgreSQL 15, isso se aplicava apenas a
row_number(),rank(),dense_rank(),count()ecount(*) - No exemplo
percent_rank() <= 0.01, o PG15 tratou a condição comoFilterna subconsulta, e oWindowAggprocessou todas as 50.000 linhas - O PG16 usa a mesma condição como
Run Condition, interrompendo a execução deWindowAggantecipadamente - O tempo de execução caiu de 84.358ms no PG15 para 19.454ms no PG16, mais de 4x mais rápido
- O PostgreSQL 15 passou a permitir encerrar cedo a execução de
Tabelas particionadas e tratamento de DISTINCT trivial
-
Remoção de LEFT JOIN em tabelas particionadas
- O PostgreSQL já consegue há muito tempo remover
LEFT JOINquando ele não é necessário para a consulta e não pode causar duplicação de linhas - Antes do PostgreSQL 16, a remoção de LEFT JOIN não era suportada para tabelas particionadas
- Isso acontecia porque faltava, para tabelas particionadas, a prova necessária de que as linhas internas não poderiam duplicar as externas
- O planejador do PostgreSQL 16 passa a aplicar a otimização de remoção de
LEFT JOINtambém em tabelas particionadas - Essa otimização pode ser especialmente útil em views
- porque uma view pode ter muitas colunas, mas a consulta real nem sempre precisa ler todas elas
- No exemplo, o plano do PG15 inclui o join com
part_tab, enquanto o plano do PG16 faz apenas uma varredura sequencial emnormal_table
- O PostgreSQL já consegue há muito tempo remover
-
DISTINCT com resultado garantidamente único tratado como Limit
- O planejador do PostgreSQL pode omitir o nó de plano de remoção de duplicatas se conseguir detectar que todas as linhas têm o mesmo valor
- O PostgreSQL 16 aproveita o fato de que, quando todas as colunas alvo de
DISTINCTestão fixadas por condições de igualdade na cláusulaWHERE, o resultado só pode conter aquele mesmo valor e, portanto, pode ser tratado comLIMIT 1 - Na consulta de exemplo
SELECT DISTINCT a,b,c FROM abc WHERE a = 5 AND b = 5 AND c = 5, cada coluna deDISTINCTestá restrita ao mesmo valor - O PG15 lê o resultado inteiro e o reduz a 1 linha com o operador
Unique - O PG16 usa
Limite varredura sequencial para retornar apenas 1 linha - O tempo de execução caiu de 30.381ms no PG15 para 0.025ms no PG16, mais de 1200x mais rápido
Uso ampliado de Incremental Sort após Merge Join
- Antes do PostgreSQL 16, ao considerar
Merge Join, o planejador só usava a ordem do join quando ela correspondia exatamente às exigências das operações superiores deDISTINCT,GROUP BYouORDER BY - Essa regra não refletia bem o fato de que
Incremental Sortpode aproveitar entradas parcialmente ordenadas nas operações superiores - O PostgreSQL 16 flexibiliza essa regra de consideração da ordem de
Merge Join: em vez de exigir correspondência exata, basta que pelo menos uma coluna inicial esteja ordenada corretamente - Com isso, o planejador pode usar Incremental Sort com mais frequência para adaptar o resultado de
Merge Joinàs necessidades da operação superior- Como o Incremental Sort trabalha com entradas parcialmente ordenadas e ordena em pequenos lotes, ele pode reduzir o uso de memória e o número de comparações em relação a uma ordenação completa
- No exemplo, o PG15 usou uma ordenação completa
SortapósMerge Join, enquanto o PG16 usouIncremental Sort- O pico de memória do
Incremental Sortno PG16 foi de 26KB - O tempo de execução caiu levemente de 1010.738ms no PG15 para 915.589ms no PG16, mas o uso de memória na ordenação diminuiu bastante
- O pico de memória do
Como isso se aplica na prática
- As 10 melhorias do planejador no PostgreSQL 16 vêm todas ativadas por padrão
- Cada otimização é aplicada em todos os casos possíveis ou escolhida seletivamente quando o planejador entende que ela será útil
- Se você usa uma versão anterior do PostgreSQL, vale executar sua carga real no PostgreSQL 16 para verificar quais consultas ficam mais rápidas
- Feedback de uso real pode ser compartilhado na mailing list pgsql-general@postgresql.org
1 comentários
Comentários do Hacker News
Seria muito bom se o planejador de consultas do PostgreSQL pudesse replanejar a consulta no meio da execução
Consultas patologicamente lentas muitas vezes acontecem porque o planejador não conhece as informações necessárias sobre a distribuição dos dados e estima mal o custo, o que facilmente gera uma diferença de 1000x, como uma execução levar 1 ms em vez de 1 segundo
Como as estatísticas da tabela nunca podem ser 100% precisas, seria bom que, se após iniciar a consulta o progresso estivesse mais lento do que o esperado, o planejador pudesse receber de novo informações do andamento atual, como número de páginas varridas e tuplas correspondidas, para criar um novo plano
Mas o PostgreSQL envia os resultados em streaming, em vez de produzir tudo e só então enviar, então mudar o plano no meio exigiria rastrear os resultados já enviados ao cliente, o que implica uma grande mudança de infraestrutura
Além disso, o cliente pode até inverter a direção no meio da consulta e pedir novamente os resultados anteriores em ordem inversa, o que aumenta ainda mais a complexidade
Isso porque nem sequer há garantia de que o novo plano retornará as mesmas tuplas. Por exemplo, em
SELECT * FROM table LIMIT 10, semORDER BY, não é determinístico quais tuplas sairãoTalvez fosse mais fácil empilhar X tuplas em uma fila e só começar a enviá-las quando a fila encher. Depois que a fila estiver cheia, consideraríamos que já é tarde demais para replanejar e o plano atual ficaria fixo
O usuário poderia ajustar X para ganhar mais tempo para mudar o plano, em troca de usar mais memória e aumentar a latência até a primeira tupla
A nova consulta não poderia simplesmente pular os N primeiros resultados; ela teria de conferir cada linha já enviada contra um dicionário
Uso esta ferramenta para visualizar consultas: https://explain.dalibo.com/
Também existe https://www.pgexplain.dev/; antes a saída parecia pior, mas agora os dois parecem semelhantes
Queria saber se existe alguma ferramenta de sanitização de planos de execução que ajude nessa situação
Melhorias no planejador de consultas são sempre bem-vindas, e essa é uma parte muito importante do banco de dados. Claro que normalmente isso fica mais visível quando não funciona do jeito que eu quero
Uma parte que pessoalmente me frustrou bastante é o JIT das versões recentes do PostgreSQL. As heurísticas para decidir quando usá-lo não parecem nada robustas
Vi isso em consultas típicas geradas por ORM: a consulta em si é simples, mas acaba puxando muitas tabelas por causa dos joins. Sem JIT, termina em poucos milissegundos, mas o JIT gasta mais 1 a 1,5 segundo e fica absurdamente lento até com pouco volume de dados
Agora já sei que basta desativar o JIT, mas para usuários que ainda não entendem por que está lento isso pode prejudicar bastante a impressão sobre o PostgreSQL. Gosto do PostgreSQL, mas deixar o JIT ativado por padrão parece arriscado demais
No PG16, ele olha apenas para o custo total estimado do plano e não considera quantas expressões precisam ser compiladas
Compilar algumas expressões é rápido, mas se você consulta uma tabela particionada com centenas de partições e todas elas entram no plano, o compilador JIT passa a ter muito trabalho
Tenho um código com um colega para melhorar isso, mas neste momento não é certo que ele entre no PG17
Mesmo procurando discussões sobre JIT na mailing list do PostgreSQL, não encontrei uma razão convincente
Em cargas de trabalho OLTP, faz sentido desativar o JIT
Não uso ORM, então não é simplesmente um problema de padrões de consulta estranhos
Em compensação, a paralelização de consultas pode ser realmente útil e, acima de tudo, raramente causa dano
Atualizei alguns pacotes com
apte, de repente, uma consulta grande executada a cada 5 minutos começou a falhar. Mais especificamente, o PostgreSQL simplesmente encerrava a conexão silenciosamente no meio da execução, sem nem registrar logsTestando manualmente com
EXPLAIN, confirmei que só quebrava a variante da consulta que passava a usar JIT; a que não usava continuava funcionando. Ao desativar o JIT, tudo voltou ao normalFico curioso sobre com frequência essas mudanças realmente ajudam em consultas reais. Em particular, a mudança de “usar
Limitem vez deUniquepara implementarDISTINCTquando possível” parece algo que só se aplicaria a consultas muito tolasFico me perguntando se os desenvolvedores do PostgreSQL têm alguma fonte de informação para avaliar isso
Se a melhoria de DISTINCT tornar o sistema mais robusto contra consultas ruins, já há muito a ganhar. Não vai corrigir todos os problemas, mas qualquer melhoria é bem-vinda
pgsql-hackersConcordo que a chance de se aplicar com frequência é baixa, mas o lado bom é que detectar se ela se aplica era tão simples quanto verificar se um ponteiro é
NULLA detecção é muito simples e, na maioria dos casos, não se aplicará, mas quando for aplicável pode trazer um ganho de performance considerável
Talvez não seja um problema muito comum, mas eu não me surpreenderia se aparecesse de vez em quando
select distinct email from users where email = ?Acho que nunca havia mais de 100 linhas com o mesmo e-mail. A maioria era de usuários de teste que poderiam ter sido apagados, mas enfim, me desviei do assunto
Seria bom se o PostgreSQL tivesse um modo estrito para testes de aplicações. Um modo que, olhando apenas para a própria consulta, independentemente de estatísticas, retornasse erro se existir um índice que melhoraria a consulta assintoticamente e esse índice estiver ausente
Também seria bom ter um comando
CREATE INDICES FORpara criar esse índice em upgrades da aplicação, e um modo de criação automática de índices para uso interativo e de desenvolvimentoNo geral, o sistema deveria ser projetado para que execuções assintoticamente subótimas jamais aconteçam
Não entendo por que não implementam hints
pg_hint_plan. O perigo dos hints é que, mesmo estando corretos quando foram escritos, eles podem piorar as coisas quando o tamanho da tabela ou a distribuição dos dados mudaPelo que lembro de discussões antigas sobre hints, não havia oposição geral se fosse uma forma que não amarrasse demais o planner e ainda permitisse adaptação a mudanças nos dados subjacentes
Por exemplo, em vez de especificar que um determinado predicado combina com 10 linhas, informar que há correlação entre duas colunas
https://news.ycombinator.com/item?id=2179433 (60 comentários, 2011)
A posição oficial no wiki do PostgreSQL está em https://wiki.postgresql.org/wiki/OptimizerHintsDiscussion
A posição é: “não há interesse em hints exatamente da forma como costumam ser implementados em outros bancos de dados”
Entre os problemas dos sistemas de hints existentes estão piora na manutenção do código da aplicação, dificuldade para upgrades, incentivo a maus hábitos de DBAs e falta de adequação ao crescimento do volume de dados
Não quero culpar essa posição, mas é frustrante quando o PostgreSQL escolhe um plano ruim e você não consegue convencê-lo a fazer a escolha razoável
Um amigo meu, DBA da Microsoft voltado para empresas de médio porte, disse que não dá para fazer trabalho sério com PostgreSQL. Disse até que ficou chocado ao descobrir que o PostgreSQL nem tinha um planejador de consultas
Deixando a zombaria de lado por um momento, fico me perguntando se há alguma plausibilidade na afirmação mais ampla de que o MSSQL consegue lidar com uma escala para a qual o PostgreSQL seria inadequado. Minha intuição diz que isso é absurdo, mas não sou DBA nem de longe
Eles historicamente resolveram problemas jogando dinheiro e hardware, ou seja, mais dinheiro, até a situação se resolver. Claro que há muita tecnologia inteligente ali, mas no fundo houve muito mais engenharia investida por muito mais tempo
Eles conseguem escalar horizontalmente para além do que o PostgreSQL consegue fazer de forma razoável
Dito isso, o PostgreSQL está alcançando esse nível, e pode-se argumentar que MySQL/MariaDB sempre tiveram uma boa história nessa área. As opções de escala horizontal continuam melhorando
Hoje já ficou mais fácil operar clusters PostgreSQL de vários terabytes com poucas máquinas e lidar com tráfego alto, deixando o “big data” em bancos mais especializados. O jeito antigo de enfiar tudo em MSSQL/Oracle talvez já esteja um pouco ultrapassado
O que seu amigo pode ter querido dizer é que o PostgreSQL não tem uma forma de cachear ou fixar planos de consulta. O PostgreSQL replana cada instrução, a menos que você use instruções preparadas manualmente, e mesmo assim isso só funciona por conexão
O MSSQL faz cache e reutiliza planos há muito tempo, então o planejador pode gastar mais tempo elaborando o plano. Ele também tem hints e permite fixar planos
O PostgreSQL realmente precisa de hints. Mesmo com um ótimo otimizador, às vezes eu sei mais e quero conseguir fazê-lo me ouvir
Além disso, o PostgreSQL não tem um verdadeiro índice clusterizado, e toda tabela é heap. No MSSQL isso é usado com bastante frequência; normalmente você define a chave primária como índice clusterizado, então a própria tabela vira o índice e não há indireção ao buscar pela chave
Curiosamente, no SQLite é o contrário: a tabela sempre tem um índice clusterizado, você o crie ou não, e o MSSQL deixa você escolher entre heap e tabela organizada por índice
Há exemplos de bancos PostgreSQL muito grandes funcionando bem, então o PostgreSQL certamente consegue escalar
Dito isso, o SQL Server tem recursos que o PostgreSQL não tem, e se eles forem importantes, pode ser uma opção melhor para certos casos de uso. No fim, são bancos diferentes com pontos fortes e fracos diferentes
A princípio eu ia escrever que teria recomendado migrar a empresa para PostgreSQL se não fosse por um aplicativo de fornecedor que exigia SQL Server
Mas então percebi o quanto daria trabalho substituir coisas que a Microsoft já inclui, como reporting services, integration services, jobs, integração com AD e service broker.
notify/listennão tem tipos de mensagemJá não uso mais analysis services, mas quando usava isso também teria sido difícil de substituir
Esse tipo de coisa prende as pessoas. Nem faço ideia de quanto tempo levaria para substituir tudo isso, e gastar um ano trocando algo que você já tem não costuma dar um bom retorno sobre o investimento
Fico me perguntando por que isso foi publicado no citusdata em vez de no postgresql.org. Não sei se é algo só para funcionalidades pagas ou algum acréscimo open source
Quando será que vamos poder usar índices para acelerar consultas com
IS NOT DISTINCT FROM;)