Estou usando o Postgres 13.3 com consultas internas e externas que produzem apenas uma única linha (apenas algumas estatísticas sobre contagens de linhas).
Não consigo descobrir por que o Query2 abaixo é muito mais lento que o Query1. Eles devem basicamente ser quase exatamente os mesmos, talvez alguns ms de diferença no máximo ...
Consulta1: leva 49 segundos
WITH t1 AS (
SELECT
(SELECT COUNT(*) FROM racing.all_computable_xformula_bday_combos) AS all_count,
(SELECT COUNT(*) FROM racing.xday_todo_all) AS todo_count,
(SELECT COUNT(*) FROM racing.xday) AS xday_row_count
OFFSET 0 -- this is to prevent inlining
)
SELECT
t1.all_count,
t1.all_count-t1.todo_count AS done_count,
t1.todo_count,
t1.xday_row_count
FROM t1;
Query2: leva 4 minutos e 30 segundos
E eu adicionei apenas uma linha:
WITH t1 AS (
SELECT
(SELECT COUNT(*) FROM racing.all_computable_xformula_bday_combos) AS all_count,
(SELECT COUNT(*) FROM racing.xday_todo_all) AS todo_count,
(SELECT COUNT(*) FROM racing.xday) AS xday_row_count
OFFSET 0 -- this is to prevent inlining
)
SELECT
t1.all_count,
t1.all_count-t1.todo_count AS done_count,
t1.todo_count,
t1.xday_row_count,
-- the line below is the only difference to Query1:
util.divide_ints_and_get_percentage_string(todo_count, all_count) AS todo_percentage
FROM t1;
Antes desse ponto, e com algumas colunas extras na consulta externa (o que deveria ter feito quase nenhuma diferença), a consulta inteira era incrivelmente lenta, como 25 minutos, o que acho que foi devido ao inlining talvez? Daí a OFFSET 0
adição em ambas as consultas (o que ajuda muito).
Eu também tenho trocado entre usar os CTEs acima vs subconsultas, mas com o OFFSET 0
incluído não parece fazer nenhuma diferença.
Definições das funções que estão sendo chamadas em Query2:
CREATE OR REPLACE FUNCTION util.ratio_to_percentage_string(FLOAT, INTEGER) RETURNS TEXT AS $$ BEGIN
RETURN ROUND($1::NUMERIC * 100, $2)::TEXT || '%';
END; $$ LANGUAGE plpgsql IMMUTABLE;
CREATE OR REPLACE FUNCTION util.divide_ints_and_get_percentage_string(BIGINT, BIGINT) RETURNS TEXT AS $$ BEGIN
RETURN CASE
WHEN $2 > 0 THEN util.ratio_to_percentage_string($1::FLOAT / $2::FLOAT, 2)
ELSE 'divide_by_zero'
END
;
END; $$ LANGUAGE plpgsql IMMUTABLE;
Como você pode ver, é uma função muito simples, que está sendo chamada apenas uma vez, a partir de uma única linha que a coisa toda produz. Como isso pode causar uma desaceleração tão grande? E por que está afetando se o Postgres inline a subconsulta inicial / CTE? (Ou o que mais pode estar acontecendo aqui?)
Além disso, não importa o que a função faz, simplesmente substituí-la por uma função que não faz nada além de retornar uma TEXT
string hello
causa exatamente a mesma lentidão da consulta interna inicial. Portanto, não se trata de algo que a função "faça", mas mais como algum tipo de efeito "gato de Schrödinger", onde coisas na consulta externa estão afetando como a consulta interna é executada inicialmente. Por que uma pequena mudança simples na consulta externa (que basicamente tem efeito zero no desempenho) afeta a consulta interna inicial?
EXPLAIN ANALYZE
saídas:
- Consulta1: https://explain.depesz.com/s/bq7u
- Consulta2: https://explain.depesz.com/s/9w3rY
A função inlining é importante e se aplica aqui também. Sua função PL/pgSQL não pode ser embutida. (Além de ser um exagero chamar outra função para a expressão trivial.) Mas como ainda é muito barato e só é chamado uma vez, não é o problema aqui.
Se você usa o
OFFSET 0
hack ouWITH CTE t1 AS MATERIALIZED
o , isso evita avaliações repetidas. (Se você for usar oOFFSET 0
hack, você também pode usar uma subconsulta um pouco mais barata, mas a maneira limpa no Postgres moderno é umaMATERIALIZED
CTE.) Esse também não é o problema . (Ou não mais, depois que você impediu com sucesso a avaliação repetida.)A questão mais importante é o paralelismo . As funções do usuário são
PARALLEL UNSAFE
por padrão.O manual:AudaciosoMinha ênfase em
Seu 1º plano de consulta (rápido) mostra 2x
Parallel Seq Scan
e 1xParallel Index Only Scan
.Seu segundo plano de consulta (lento) não tem consultas paralelas. Dano feito.
Solução
Marque suas funções
PARALLEL SAFE
(porque elas se qualificam!) e o problema desaparece. Relacionado:Melhor solução
Executei testes de desempenho com algumas variantes. Ver:
Esta função equivalente é substancialmente mais rápida e pode ser embutida:
Mais importante, é o
LANGUAGE sql
que permite a função inlining , (ao contrárioLANGUAGE plpgsql
de ). Ver:Notavelmente , precisamos desse elenco explícito
::text
. O operador de concatenação||
é resolvido para uma das várias funções internas, dependendo dos tipos de dados envolvidos, e nem todas sãoIMMUTABLE
. Sem o cast explícito, o Postgres escolheria uma variante que é onlySTABLE
, e isso discordaria da declaração da função e impediria o inlining da função. Detalhes sorrateiros! Relacionado:Corrigido um problema de lógica enquanto estava nisso:
$2 = 0
verifica a divisão por zero corretamente (ao contrário$2 > 0
de ). Agora,count(*)
nunca pode ser negativo, mas como você coloca a lógica em uma função, ela fica isolada dessa pré-condição.Ou apenas coloque a expressão simples na consulta diretamente. Nenhuma chamada de função. Isso não é suscetível a nenhum dos problemas mencionados.
Parece que você atingiu algum tipo de barreira de otimização no PostgreSQL em que suas funções, em vez de serem avaliadas uma vez após o CTE, estão sendo avaliadas várias vezes!
O que eu faria no seu caso é o seguinte:
O uso dos casts to
::REAL
significa que você obterá "apenas" uma porcentagem com precisão de 6 casas decimais (consulte a documentação do PostgreSQL aqui ), mas raramente encontrei situações em que mais do que isso fosse necessário.FLOAT
sem precisão é, de fato, aDOUBLE PRECISION
(15 lugares).Da documentação:
Existem outras maneiras e meios de fazer o que você precisa...
Dê uma olhada aqui para algumas sugestões do site PostgreSQL se você não precisar de uma contagem exata e uma porcentagem exata . E então há (mais uma) resposta magistral de @Erwin Brandstetter aqui - ele fornece algumas maneiras de atingir seu objetivo e explica os prós e contras de cada um ...
Alguns pontos de encerramento:
Suas funções: você parece ter muitos problemas para executar o que é (ou pelo menos deveria ser) as etapas finais de formatação muito antes de serem necessárias. Muitos argumentariam que o que você está fazendo em suas funções deve ser feito na camada cliente/apresentação. Eu me absteria de realizar esse tipo de manipulação pelo menos até a última etapa do SQL! Bancos de dados são para armazenar dados, não para apresentá-los!
Outra solução (se você deseja insistir absolutamente em usar suas funções) pode ser envolver sua consulta em outra
SELECT
e fazer com que as funções operem nos resultados dessa consulta - isso deve remover a cerca de otimização (veja o exemplo aqui )! Um pouco complicado talvez, mas suas funções também são!Após sua edição, o que você chama
"some kind of "Schrödinger's cat" effect
é, de fato,"optimisation fence"
um problema com os CTEs há anos. Ele deveria ser corrigido pelaWITH cte_name AS [ NOT ] [MATERIALIZED] (...
diretiva (veja aqui ). A partir dessa resposta, sua consulta não está livre de efeitos colaterais!Agora, você dirá "mas, tudo o que ele faz é calcular uma porcentagem...", mas o otimizador não pode saber disso antecipadamente e "não se arrisca" e parece estar avaliando sua função várias vezes em vez de uma.
Por fim, apontei que você obviamente não nos forneceu todas as informações necessárias - há nomes de tabelas no PLAN que não aparecem em sua pergunta, o que implica para mim que você está consultando
VIEW
s, o que pode muito bem ser um fator de confusão.eu sugiro que você forneça um caso de teste em dbfiddle.uk (com tabelas base subjacentes), as visualizações que você cria nessas e em todas as suas consultas e funções - caso contrário, nenhuma ajuda adicional poderá ser fornecida.
Sua analogia com
"Schrödinger's cat"
talvez seja particularmente adequada - não temos todas as informações - faça o postuladoVIEW
s postulados existem ou não? Eles sãoVIEW
s emVIEW
s? Eles fazemVIEW
alguma coisa? Se aVIEW
éDROP
acionado no meio de uma floresta e ninguém o ouve, será que foi realmenteDROP
acionado?Sem uma divulgação completa, podemos ser de pouca ajuda. No que me diz respeito, respondi à pergunta conforme solicitado e (de acordo com você) forneci soluções que funcionam. Concedido, pode não ser totalmente satisfatório, mas com o que temos, é o melhor que você vai conseguir!