10 anos de melhorias no otimizador do PostgreSQL
(rmarcus.info)- Comparando a latência de consulta no 90º percentil com o Join Order Benchmark do PostgreSQL 8 ao 16, confirma-se empiricamente a melhora de longo prazo no desempenho de cauda
- Em relação ao PostgreSQL 8, o 16 reduziu a latência de cauda quase pela metade, e o intervalo de 13 a 16 permaneceu em um nível geralmente estável
- Na análise de regressão, observou-se uma melhora média de 15% no desempenho a cada avanço de uma versão principal, embora um modelo linear possa não explicar bem o padrão das mudanças
- O experimento fixou as condições com GCC 13.2, Docker no Arch Linux,
shared_buffersde 8GB ework_memde 8MB, focando na qualidade do otimizador de consultas - Ao interpretar a magnitude das melhorias, é preciso considerar não só o otimizador, mas também mudanças no motor de execução, como workers paralelos e compilação JIT
Configuração do benchmark do PostgreSQL 8~16
- O objeto da análise são as versões principais de 8 a 16 do PostgreSQL, um otimizador de consultas open source
- No benchmark foi usado o Join Order Benchmark, um conjunto de consultas com muitas junções complexas
- Esse benchmark foi introduzido no artigo “How Good are Query Optimizers, Really?”
- Cada versão do PostgreSQL foi compilada com GCC 13.2 dentro de um contêiner Docker no Arch Linux
- O ambiente de medição foi ajustado para observar a qualidade do otimizador de consultas, mais do que desempenho de índices ou de I/O
shared_buffersfoi configurado em 8GB, grande o suficiente para conter todo o banco de dadoswork_memfoi fixado em 8MB em todas as versões
- Cada consulta foi executada uma vez para aquecer o cache e, em seguida, registrou-se a latência mediana de mais 5 execuções
- Em cada versão principal foi usada a versão menor mais recente
- Por exemplo, para PostgreSQL 8 foi usada a 8.4.22
- Essas versões menores normalmente saíram depois da nova versão principal, mas em geral incluem apenas correções de bugs, sem novos recursos ou melhorias de desempenho
Resultados da medição e interpretação
- O desempenho de cauda do PostgreSQL melhorou bastante no geral
- Comparando PostgreSQL 8 e 16, a latência de cauda caiu quase pela metade
- Do PostgreSQL 13 ao 16, o nível se manteve geralmente estável
- A análise de regressão foi usada para verificar se a tendência de queda entre o número da versão principal e a latência de consulta é significativa, além de quantificar o ganho por versão
- Segundo a regressão linear, cada nova versão principal trouxe em média uma melhora de 15% no desempenho no Join Order Benchmark
- Ainda assim, um modelo linear pode ser inadequado para medir o padrão real das mudanças
- É difícil explicar todas as melhorias apenas pelo otimizador de consultas
- Melhorias no motor de execução, como workers paralelos e compilação JIT, também afetam o desempenho
- Como os planos de execução de cada consulta do JOB mudaram ao longo dos anos continua sendo tema para uma análise separada
- Ao migrar do PostgreSQL 8 para o 16, há chance de a latência de cauda da carga de trabalho cair bastante
- Em comparações de pesquisa, é importante notar que o próprio PostgreSQL continua se fortalecendo como linha de base
- Neo e Bao foram comparados ao PostgreSQL 11, mas pesquisas mais recentes comparam com PostgreSQL 14, 15 e 16
- Mesmo que uma técnica antiga melhore 30% em relação ao PostgreSQL e uma técnica recente melhore 25%, a técnica recente pode ter sido comparada com um PostgreSQL mais forte
- Os valores medidos originais podem ser consultados nos raw data
1 comentários
Comentários do Hacker News
Uso o Postgres há 15 anos e passei a maior parte da carreira modelando e resolvendo problemas de otimização matemática, e acho que há três pontos centrais neste tema
Todo problema de otimização precisa de dados de custo, e quanto mais e melhores forem esses dados, melhor. O Postgres teve melhorias, como estatísticas entre colunas, mas ainda há lacunas grandes, como a latência de chamadas de sistema. A latência para ler páginas do disco varia muito entre sistemas, mas o Postgres não mede isso diretamente e depende de valores de configuração. Também faltam estatísticas de chave estrangeira, então joins que seguem chaves estrangeiras não deveriam gerar planos ruins, mas isso ainda acontece às vezes
Especialmente para consultas grandes e caras, é preciso haver planejamento tardio ou planejamento de cenários alternativos. Hoje o plano é fechado antes da execução, mas o número de linhas ou as estimativas de cardinalidade obtidas nas fases iniciais da execução podem melhorar bastante o plano das fases posteriores
Aprendizado de máquina também é uma área com espaço para melhorias, mas as tentativas que vi até agora não foram impressionantes. Em vez de usar aprendizado de máquina no plano em si, ele deveria ser usado em descoberta e estimativa de custos. É preciso criar modelos de custo melhores e fazer o mecanismo de otimização usar esses dados
No caso de planejamento tardio/alternativo, fico me perguntando se execução adaptativa de consultas é uma abordagem razoável. Dá para fazer com que as informações do começo da execução influenciem o plano posterior, mas escolher errado os primeiros joins é algo comum, e me preocupa que seja difícil se recuperar disso sem coisas como Yannakakis/SIPs
Quanto a “aprendizado de máquina para otimização de consultas”, claramente tenho meus vieses. Ainda assim, todas as abordagens de “aprendizado de máquina para planejamento” que vi acabam, por baixo dos panos, usando aprendizado de máquina para descoberta/estimativa de custos. Essas abordagens tentam equilibrar os dados que coletam, isto é, exploração, com a qualidade dos planos que produzem, isto é, aproveitamento. Curiosamente, se você usar aprendizado de máquina de um jeito totalmente separado do planejamento, as estimativas ficam mais precisas, mas os planos reais de consulta ficam piores: https://people.csail.mit.edu/tatbul/publications/flowloss_vl...
Tenho interesses nessa área, então é bom considerar isso ao avaliar minha opinião
Ainda não sei por que a estimativa errou tanto, mas se fosse possível trocar de loop aninhado para hash join quando a contagem de linhas passasse de algum limite, isso provavelmente ajudaria muito a evitar planos desastrosos
Você está falando de problemas na ordem dos joins?
O otimizador de consultas do Postgres tenta reduzir o número de páginas lidas do disco e o número de páginas escritas no disco como resultado intermediário. Por isso, parece errado usar shared buffers grandes o bastante para comportar todos os dados e então fazer benchmark do otimizador de consultas
Nesse caso, você estaria medindo não a qualidade do plano de consulta gerado, mas a velocidade do otimizador e do processador de joins. Na prática, eu nem me surpreenderia se os planos gerados em cada versão fossem todos iguais e só a velocidade de execução tivesse sido medida
O custo é uma unidade arbitrária feita para se correlacionar com tempo gasto, não com número de leituras de disco, então comparar planos com tudo já em RAM também é totalmente válido. Por convenção, uma leitura de uma página de disco é escalada como 1,0, mas isso é diferente de dizer que “o otimizador minimiza o número de leituras de páginas de disco”. Também poderia ter sido definido como 1,0 para 1 ms em uma máquina qualquer
O otimizador do PG tenta reduzir não só o número de páginas lidas do disco, mas também o número de tuplas que a CPU examina, a quantidade de avaliações de expressões condicionais etc., e todos esses números são combinados em um “custo” que vira a função que o otimizador minimiza
Medições de desempenho com cache frio e cache quente podem dar resultados diferentes, e este experimento é claramente um cenário de cache quente. Mas o cache frio também tem os problemas mencionados. No tamanho de dados do Join Order Benchmark, o efeito das melhorias de B-tree do PG em economizar algumas operações de I/O pode acabar dominando as melhorias baseadas em CPU
Como referência, o plano da consulta no percentil 90 de latência mudou de um plano que usava loop join e merge join no PG 8.4 para um plano que usa hash join no PG 16, e essa consulta já não é mais a consulta do percentil 90. Isso pode ser visto ao menos como alguma evidência de melhoria no otimizador
O texto menciona o compilador JIT do PostgreSQL, mas até agora eu só vi ele piorar o desempenho das consultas. Já coloquei sua desativação na checklist de instalação
No fim, descobrimos que o Homebrew instalava o Postgres sem suporte a JIT, e em uma máquina de desenvolvedor certas consultas terminavam em 200 ms, enquanto em ambientes com JIT ativado levavam 4 a 5 segundos. Eu não uso Postgres tão a fundo, então levei um tempo para descobrir a causa, e desde então sempre desligo o JIT e não penso mais nisso
No PostgreSQL, também dá para configurar o limiar de ativação do JIT, então você pode aumentar o critério para ele entrar em ação
Se desse para compilar de forma assíncrona para consultas futuras, provavelmente seria menos prejudicial. Na verdade, JIT em geral, especialmente backends de otimização, tende a funcionar mais próximo desse modelo
Interessante, mas o esquema de numeração de versões do Postgres mudou no v10. 9.6, 9.5, 9.4, 9.3, 9.2, 9.1, 9.0, 8.4, 8.3, 8.2, 8.1 e 8.0 são, na prática, todos versões principais distintas
Também seria interessante ver como o desempenho mudou nessas versões
Isso pode até ter limitado o avanço, mas atualizações anuais que exigem mais downtime ou reindexação não são nada agradáveis, e talvez seja por isso que muitos sites adiem o upgrade até o fim do suporte da versão anterior. Especialmente usuários do AWS RDS
Os upgrades com replicação lógica desde o v10 têm vantagens de disponibilidade, mas são projetos grandes com custo inevitável e risco significativo, a menos que o esquema seja relativamente simples
Por exemplo, PG 8.2 e 8.1 são versões principais diferentes, mas eu as interpretei como se fossem versões menores. A principal razão para isso foi reduzir o número de versões a testar, e concordo que uma análise mais completa deveria testar cada versão principal real
Foi dito que “é claro que essa melhoria não se deve inteiramente ao otimizador de consultas”, mas seria interessante ver se houve mudanças nos planos de execução entre as versões
Isso me lembrou a lei de Proebsting: https://proebsting.cs.arizona.edu/law.html
Imagine qual seria o impacto ambiental de otimizar o desempenho do Python em 1%. Quanto CO2 na atmosfera isso reduziria? Provavelmente mais do que a pegada ambiental combinada de você, sua família e seus amigos. Talvez até equivalente à da cidade inteira onde você mora. E tudo isso porque alguém gastou tempo implementando alguns truques com operações de bits
Será porque 15% parece pouco? Neste contexto, não é pouco nem de longe. É menor que os 60% da lei linkada, e fica ainda menor se você dividir como 15/10, mas não dá para comparar desempenho do Postgres com avanço de hardware. Para igualar aqui um ganho de 1% no que está sendo medido, seria preciso uma melhoria enorme de hardware
Não acho que essa lei seja tão absurda quanto algumas pessoas dizem, mas ela fala sobre tempo de compilação de linguagens de programação. Eu não compararia algo relativamente pouco importante assim com armazenamento e consumo de dados, que dá para dizer que estão entre as coisas mais importantes da ciência da computação
O único ponto de comparação apresentado é a lei de Murphy. Fico curioso sobre a diferença de custo entre desenvolver hardware mais rápido e continuar melhorando compiladores. Dependendo de como o retorno sobre investimento se comparar, por exemplo em dólares por ponto percentual de ganho de desempenho, talvez essa “lei” tenha algum peso
Por outro lado, este texto sobre Postgres parece mostrar retornos decrescentes na otimização, o que contraria a premissa dessa “lei” de ganho constante ano após ano. Ao mesmo tempo, isso também poderia servir para confirmar a insinuação de Proebsting de que, no longo prazo, otimização é um investimento ruim
Essa análise está um pouco confusa. Não entendi como chegaram a uma tendência de queda nos dados que não aparece no gráfico
A mediana parece cair um pouco nas primeiras versões e depois voltar a subir nas versões mais recentes. O R² é muito baixo, então a correlação não parece convincente. Basicamente, parece que a latência de cauda melhorou e o resto depende do ambiente
A interpretação de que “a latência de cauda melhorou e o resto depende do ambiente” é válida, mas eu a consideraria uma leitura conservadora. Claro, para muitas aplicações — talvez a maioria — a latência de cauda é muito importante. Além disso, a latência de cauda também é justamente o alvo principal dos engenheiros do otimizador: reduzir o tempo de execução das consultas mais demoradas
Como é a otimização de consultas? Fico curioso se a otimização acontece no nível do SQL ou no nível do algoritmo
Isso parece acontecer porque várias consultas SQL diferentes podem ser transformadas no mesmo “comando” ou plano de execução, e a própria semântica do SQL não oferece muito espaço para otimização no nível da linguagem.
Como foi dito em outra resposta, uma das decisões importantes é se é possível trocar um scan completo da tabela por uma busca em índice ou um index scan.
Por exemplo, se for necessário fazer um scan completo da tabela e for preciso realizar um cálculo considerável para cada linha para decidir se ela entra no conjunto de resultados, o otimizador pode trocar esse scan completo por um scan paralelo da tabela e mesclar os resultados de cada tarefa paralela.
Ao escrever código de alto desempenho para compiladores, é preciso saber como o otimizador do compilador transforma o código-fonte em código de máquina. Assim, você pode preferir códigos que o otimizador trata bem e evitar padrões que geram código de máquina mais lento. No fim, o otimizador é programado para detectar e transformar certos padrões.
Com o otimizador de consultas e os planos de execução acontece a mesma coisa. É preciso aprender quais padrões o otimizador de consultas do banco de dados que você usa consegue tratar para gerar planos de execução eficientes
user_idé xx, ele escolhe entre ler a tabela inteira e filtrar ou usar uma estrutura de dados dedicada.Usando índice, é possível encontrar em tempo logarítmico em relação ao número de linhas. Além disso, há muitas outras possibilidades, como escolher a ordem dos joins, escolher a estratégia de join e empurrar condições de filtro para a origem. Esse é o amplo campo da otimização de SQL
Com essas informações, ele define a ordem dos joins, escolhe índices e assim por diante. Joins podem ser executados com vários algoritmos, como hash, loop e merge. A opção mais barata varia conforme fatores como se um dos lados cabe na memória de trabalho, se ambos os lados já estão ordenados, por exemplo graças a um index scan, etc.
Como o site parece estar fora do ar, dá para ver isto aqui: https://web.archive.org/web/20240417050840/https://rmarcus.i...