PostgreSQL Query Profiler: como combinar plano e consulta
Muitos que já estão usando explica.tensor.ru - nosso serviço de visualização de plano PostgreSQL pode não estar ciente de um de seus superpoderes - transformar uma parte difícil de ler do log do servidor...
... em uma consulta lindamente projetada com dicas contextuais para os nós do plano correspondentes:
A transcrição da primeira parte, dedicada a problemas típicos de desempenho de consultas e suas soluções, pode ser encontrada no artigo "Receitas para consultas SQL problemáticas".
Primeiro vamos começar a colorir - e não vamos mais colorir o plano, já pintamos, já temos lindo e compreensível, mas um pedido.
Pareceu-nos que com uma “folha” tão não formatada a solicitação retirada do log parece muito feia e, portanto, inconveniente.
Especialmente quando os desenvolvedores “colam” o corpo da solicitação no código (isso é, claro, um antipadrão, mas acontece) em uma linha. Horrível!
Vamos desenhar isso de uma forma mais bonita.
E se pudermos desenhar isso lindamente, ou seja, desmontar e remontar o corpo da solicitação, então poderemos “anexar” uma dica a cada objeto dessa solicitação - o que aconteceu no ponto correspondente do plano.
Árvore de sintaxe de consulta
Para fazer isso, a solicitação deve primeiro ser analisada.
Porque nós temos o núcleo do sistema é executado em NodeJS, então fizemos um módulo para ele, você pode encontre-o no GitHub. Na verdade, essas são “ligações” estendidas aos componentes internos do próprio analisador PostgreSQL. Ou seja, a gramática é simplesmente compilada binariamente e ligações são feitas a ela a partir do NodeJS. Tomamos como base os módulos de outras pessoas - não há grande segredo aqui.
Alimentamos o corpo da solicitação como entrada para nossa função - na saída obtemos uma árvore de sintaxe analisada na forma de um objeto JSON.
Agora podemos percorrer esta árvore na direção oposta e montar uma solicitação com os recuos, cores e formatação que desejamos. Não, isso não é personalizável, mas pareceu-nos que seria conveniente.
Mapeando nós de consulta e plano
Agora vamos ver como podemos combinar o plano que analisamos na primeira etapa e a consulta que analisamos na segunda.
Vejamos um exemplo simples: temos uma consulta que gera um CTE e o lê duas vezes. Ele gera tal plano.
Isso significa que se virmos uma geração de CTE em algum lugar da solicitação e um nó em algum lugar do plano CTE, então esses nós definitivamente “lutam” entre si, podemos combiná-los imediatamente.
Problema com um asterisco: CTEs podem ser aninhados.
Existem alguns muito mal aninhados e até aqueles com o mesmo nome. Por exemplo, você pode dentro CTE A fazer CTE X, e no mesmo nível dentro CTE B faça isso novamente CTE X:
WITH A AS (
WITH X AS (...)
SELECT ...
)
, B AS (
WITH X AS (...)
SELECT ...
)
...
Ao comparar, você deve entender isso. Entender isso “com os olhos” – até ver o plano, até ver o corpo do pedido – é muito difícil. Se a sua geração de CTE for complexa, aninhada e houver grandes solicitações, ela será completamente inconsciente.
UNIÃO
Se tivermos uma palavra-chave na consulta UNION [ALL] (operador de união de duas amostras), então no plano corresponde a um nó Append, ou alguns Recursive Union.
Aquilo que está "acima" acima UNION - este é o primeiro descendente do nosso nó, que está “abaixo” - o segundo. Se através UNION temos vários blocos “colados” de uma vez, então Append- ainda haverá apenas um nó, mas não terá dois, mas muitos filhos - na ordem em que vão, respectivamente:
(...) -- #1
UNION ALL
(...) -- #2
UNION ALL
(...) -- #3
Append
-> ... #1
-> ... #2
-> ... #3
Problema com um asterisco: dentro da geração de amostragem recursiva (WITH RECURSIVE) também pode ser mais de um UNION. Mas apenas o último bloco após o último é sempre recursivo UNION. Tudo acima é um, mas diferente UNION:
WITH RECURSIVE T AS(
(...) -- #1
UNION ALL
(...) -- #2, тут кончается генерация стартового состояния рекурсии
UNION ALL
(...) -- #3, только этот блок рекурсивный и может содержать обращение к T
)
...
Você também precisa ser capaz de “destacar” esses exemplos. Neste exemplo vemos que UNION-havia 3 segmentos em nosso pedido. Assim, um UNION соответствует Append-nó, e para o outro - Recursive Union.
Dados de leitura e gravação
Está tudo definido, agora sabemos qual parte do pedido corresponde a qual parte do plano. E nestas peças podemos encontrar de forma fácil e natural aqueles objetos que são “legíveis”.
Do ponto de vista da consulta, não sabemos se é uma tabela ou um CTE, mas são designados pelo mesmo nó RangeVar. E em termos de “legibilidade”, este também é um conjunto bastante limitado de nós:
Seq Scan on [tbl]
Bitmap Heap Scan on [tbl]
Index [Only] Scan [Backward] using [idx] on [tbl]
CTE Scan on [cte]
Insert/Update/Delete on [tbl]
Conhecemos a estrutura do plano e da consulta, conhecemos a correspondência dos blocos, conhecemos os nomes dos objetos - fazemos uma comparação um a um.
Novamente tarefa "com um asterisco". Pegamos a solicitação, executamos, não temos nenhum aliases - apenas lemos duas vezes no mesmo CTE.
Analisamos o plano - qual é o problema? Por que tínhamos um pseudônimo? Nós não pedimos. Onde ele consegue esse “número numérico”?
O PostgreSQL o adiciona sozinho. Você só precisa entender isso apenas um apelido assim para nós, para efeito de comparação com o plano, não faz sentido, é simplesmente acrescentado aqui. Não vamos prestar atenção nele.
O segundo tarefa "com um asterisco": se estivermos lendo de uma tabela particionada, obteremos um nó Append ou Merge Append, que consistirá em um grande número de “filhos”, e cada um dos quais será de alguma forma Scan'om da seção da tabela: Seq Scan, Bitmap Heap Scan ou Index Scan. Mas, em qualquer caso, esses “filhos” não serão consultas complexas - é assim que esses nós podem ser distinguidos Append em UNION.
Também entendemos esses nós, reunimos-os “em uma pilha” e dizemos: “tudo que você lê no megatable está aqui e embaixo da árvore".
Nós de recepção de dados "simples"
Values Scan corresponde no plano VALUES no pedido.
Result é um pedido sem FROM como SELECT 1. Ou quando você tem uma expressão deliberadamente falsa em WHERE-block (então o atributo aparece One-Time Filter):
EXPLAIN ANALYZE
SELECT * FROM pg_class WHERE FALSE; -- или 0 = 1
Function Scan “mapear” para os SRFs de mesmo nome.
Mas com consultas aninhadas tudo é mais complicado - infelizmente, elas nem sempre se transformam em InitPlan/SubPlan. Às vezes eles se transformam em ... Join ou ... Anti Join, especialmente quando você escreve algo como WHERE NOT EXISTS .... E aqui nem sempre é possível combiná-los - no texto do plano não existem operadores correspondentes aos nós do plano.
Novamente tarefa "com um asterisco": alguns VALUES no pedido. Neste caso e no plano você obterá vários nós Values Scan.
Os sufixos “numerados” ajudarão a distingui-los uns dos outros - eles são adicionados exatamente na ordem em que os correspondentes são encontrados VALUES-blocos ao longo da solicitação de cima para baixo.
Processamento de dados
Parece que tudo em nosso pedido foi resolvido - tudo o que resta é Limit.
Mas aqui tudo é simples - nós como Limit, Sort, Aggregate, WindowAgg, Unique “mapear” um a um para os operadores correspondentes na solicitação, se eles estiverem lá. Não há “estrelas” ou dificuldades aqui.
Cadastre-se
As dificuldades surgem quando queremos combinar JOIN entre eles mesmos. Isso nem sempre é possível, mas é possível.
Do ponto de vista do analisador de consulta, temos um nó JoinExpr, que tem exatamente dois filhos - esquerdo e direito. Isso, portanto, é o que está “acima” do seu JOIN e o que está escrito “abaixo” dele na solicitação.
E do ponto de vista do plano, estes são dois descendentes de alguns * Loop/* Join-nó. Nested Loop, Hash Anti Join,... - algo parecido.
Vamos usar uma lógica simples: se tivermos as tabelas A e B que se “juntam” no plano, então na solicitação elas poderiam ser localizadas A-JOIN-BOu B-JOIN-A. Vamos tentar combinar assim, vamos tentar combinar ao contrário e assim por diante até ficarmos sem esses pares.
Vamos pegar nossa árvore de sintaxe, pegar nosso plano, dar uma olhada neles... não é parecido!
Vamos redesenhar em forma de gráficos - ah, já parece alguma coisa!
Observemos que temos nós que possuem simultaneamente filhos B e C - não nos importamos em que ordem. Vamos combiná-los e virar a imagem do nó.
Vamos olhar novamente. Agora temos nós com filhos A e pares (B + C) - compatíveis com eles também.
Ótimo! Acontece que somos esses dois JOIN da solicitação com os nós do plano foram combinados com sucesso.
Infelizmente, esse problema nem sempre é resolvido.
Por exemplo, se em uma solicitação A JOIN B JOIN C, e no plano, em primeiro lugar, foram conectados os nós “externos” A e C. Mas não existe tal operador na solicitação, não temos nada a destacar, nada a que anexar uma dica. É o mesmo com a "vírgula" quando você escreve A, B.
Mas, na maioria dos casos, quase todos os nós podem ser “desamarrados” e você pode obter esse tipo de perfil à esquerda a tempo - literalmente, como no Google Chrome quando você analisa o código JavaScript. Você pode ver quanto tempo cada linha e cada instrução levou para “executar”.
E para tornar mais conveniente o uso de tudo isso, criamos armazenamento o arquivo, onde você pode salvar e depois encontrar seus planos junto com as solicitações associadas ou compartilhar o link com alguém.
Se você só precisa trazer uma consulta ilegível para um formato adequado, use nosso “normalizador”.