Otimizando consultas no SQL Server

Mais cedo ou mais tarde precisaremos mexer naquela consulta feita provisoriamente ou com aquela pressa devido ao momento. Independente da circunstancia, podemos sempre voltar atrás e deixar a coisa melhor.
Neste artigo, falaremos do Plano de Execução disponibilizado pelo SQL Server para consultas e outras intruções do banco. E de quebra, darei algumas dicas preciosas aprendidas ao longo dos anos trabalhando com extração de dados.

Antes de começar

Gostaria de deixar bem claro que, realizaremos um estudo básico da ferramenta. Ela é bem mais completa e traz uma série de propriedades que não serão exploradas aqui. Será apenas uma introdução de conceitos para dispertar a curiosidade e a busca de novas habilidades. Porém, o que veremos já ajudará bastante quem esta chegando agora nesta praia!

A consulta está lenta. Por onde começar?

Eu acredito que esta seja uma das primeiras perguntas que vem a mente quando o desempenho começa a piorar. Certamente, muitas técnicas ou ferramentas podem ajudar neste cenário. Porém, tomei a liberdade de organizar neste artigo algumas dicas importantes que irão orientá-los nesta primeira etapa da otimização do desempenho. Aqui vão elas:

  • Garanta que todas as FKs presentes na consulta tenham indices de cobertura.
  • Filtros com uso frequente também precisam de índice.
  • Evite tabelas montadas em tempo real no FROM. Elas costumam ter alto custo para o banco. Se, mesmo assim decidir usá-la, faça o uso de filtros para diminuir a quantidade de registros processados.
  • Evite utilizar a função ROW_NUMBER para gerar ID quando a consulta retornar milhares de registros. Ela tem um alto custo para o banco e a estrategia mais simples seria concatenar informações já presentes na consulta e substituir o ID gerado pela função. Sai mais barato.
  • Evite utilizar visões com grande volume de dados juntamente com outras tabelas. A menos que, a visão sejá materializada e possua índices. Caso contrário, terá um problema crescente conforme a visão retorna mais registros.
  • Substitua o IN por EXISTS. Ele tem performance melhor quando se utiliza relacionamento envolvendo a consulta interna e a externa.
  • Obtenha o Plano de Execução da consulta ou instrução e analise-o.

O que é o Plano de Execução?

Quando pedimos para o banco gerar o Plano de Execução para uma consulta ou instrução SQL, ele fica encarregado de coletar estastisticas de cada parte ou estrutura de dados envolvida e definir o melhor caminho para a realização da tarefa. Esta tarefa é realizada pelo Otimizador de Consulta e a sua saída é o que chamamos de Plano de Execução.
Geralmente, os bancos disponibilizam os dados coletados de formas variadas. Porém, as mais comuns são no formato texto ou modo gráfico que é mais intuitivo. Neste quisito, o SQL Server dispõe de um visiual bem agradavel.
Dentro do Plano gerado, a maioria das estatísticas são coletadas a nível de operador. Como exemplos, temos:

  • Uma consulta
  • A junção de entre duas tabelas
  • Uma pesquisa em um indice

Relacionei os operadores mais comuns que podem estar presentes no plano. Lembre-se, não estão todos aqui:

Operador Descrição
no-select Consulta de onde pode derivar todo o Plano de Execução.
no-update Instrução update de onde pode derivar todo o Plano de Execução.
no-insert Instrução insert de onde pode derivar todo o Plano de Execução.
no-juncao Junção ou mesclagem entre duas fontes de dados com
resultados ordenados ou não.
no-loops Consultas aninhadas. Podem estar presentes em uma coluna ou na
WHERE como filtro.
no-index Busca em um índice não clusterizado, sendo completa ou parcial.
no-clustered-index Busca em um índice clusterizado, sendo completa ou parcial.
no-sort Operação de classificação dos dados do nó anterior.
no-hash Tabela Hash criada em tempo de execução para investigar dados
entre duas fontes.
no-top Representa a seleção das primeiras linhas de uma tabela.
no-computar-escalar Computar novos valores a partir de valores existentes em uma linha.

Como obtê-lo?

Duas formas de se obter o plano no modo gráfico:

  • Plano de Execução Estimado
  • Plano de Execução Real
    botao-plano-execucao-2

Para gerar o Plano Estimado, clique no botão circulado de vermelho (veja a figura anterior) e aguarde. A consulta não sera executada na integra, porém um plano com valores estimados será apresentado.
plano-estimado

Caso opte pelo Plano Real, clique no botão circulado de laranja e em seguida clique em Executar. Além de apresentar o resultado, também será disponibilizado o plano de execução com os valores reais. Logo a frente, falaremos um pouco mais deste tipo.
plano-real

Pré-requisitos

Antes de começar, precisamos criar algumas estruturas para a realização dos testes. Os scripts abaixo irão criar um banco novo, tabelas e a carga dos dados. Estamos assumindo que já exista o SQL Server instalado juntamente com a ferramenta SQL Server Management Studio. Siga os passos a seguir:

  1. Criação do banco de testes:
create database db_teste
go
use db_teste
go
  1. Criação das tabelas para carga dos dados:
create table tb_a 
	(
		id_a integer not null identity primary key,
		desc_a varchar(50)
	)
go

create table tb_b
	(
		id_b integer not null identity primary key,
		desc_b varchar(50),
		id_a integer
	)
go

alter table tb_b with check 
    add constraint fk_tba_tbb foreign key (id_a) references tb_a (id_a)
go

create table tb_c
	(
		id_c integer not null identity primary key,
		desc_c varchar(50),
		id_b integer
	)
go

alter table tb_c with check 
    add constraint fk_tbb_tbc foreign key (id_b) references tb_b (id_b)
go
  1. Carga das tabelas:
insert into tb_a
select 'tabela a aaaaaaa aaaaaaaaaa aaaaa'
from sys.all_objects;
go

insert into tb_b (desc_b)
select 'tabela b bbbb bbbbbb bbbbbbb bbbbbb'
from sys.all_objects;
go

update tb_b
set id_a = id_b
where id_a is null
go

insert into tb_c (desc_c)
select 'tabela c cccccc ccccccccccc  ccccccccccc'
from sys.all_objects;
go

update tb_c
set id_b = id_c
where id_b is null
go

Gerando o Plano

Vamos criar uma nova consulta utilizando as tabelas de testes que criamos no item anterior. Copie o script abaixo e cole no Studio:

select a.id_a,
a.desc_a,
b.id_b,
b.desc_b
from tb_a a
join tb_b b on b.id_a = a.id_a

Iremos gerar o Plano de Execução através da interface visual do SQL Server Management Studio. Logo a frente, falarei de outra forma para gerá-lo. Clique no botão Incluir Plano de Execução Real ou CTRL+M e em seguida Executar ou F5. O Plano Real é mais interessante por trazer no modo gráfico mais detalhes para cada operador. Por exemplo: percentual de custo, tempo de execução e linhas retornadas.
no-plano-real-1

Analisando o que foi gerado

A imagem anterior, nos traz um bom exemplo do plano apresentado em modo gráfico. Embora muito simples, ele já serve para iniciarmos os estudos.
A analise deve começar pelo canto superior, da esquerda para a direita. O primeiro operador é a consulta ou instrução principal analisada. Posicione o mouse em cima do operador desejado para ver mais detalhes.
detalhes-operador

Na janela de propriedades aberta, inumeras informações podem ser apresentadas. Porém, irei detalhar parte delas que acredito serem mais relevantes para nosso estudo inicial:

Informação Descrição
Custo estimado do operador Custo da execução do operador até aquele momento.
Custo estimado de CPU Calculado pelo Otimizador, baseado em calculos internos.
Custo estimado da subarvore Custo estimado e acumulado das operações realizadas até chegar neste operador.
Número estimado de linhas
por execuções
Número de linhas processadas no operador atual.
Tamanho estimado da linha Tamanha estimado da linha baseado em estatísticas internas do Otimizador.

Pontos de atenção

  1. Operador com alto valor no percentual de custo, sempre vale a pena analisá-lo. Principalmente quando se tratam de tabelas hash ou junções. Nestes dois casos costumamos ver situações onde as linhas recuperadas excedem a quantidade de registros esperados ou até mesmo o tamanho. Muita atenção neste ponto!
  2. Campos CLOB costumam aumentar em muito o tamanho estimado da linha em cada operador. Fique atento quando o tamanho ultrapassar os Kilo Bytes. Tive uma experiencia muito marcante com esta situação onde determinadas consultas apresentavam performance ruim. E, infelizmente em alguns registros estava vindo um campo CLOB com 3 MB que causou um I/O alto.
  3. Tabelas montadas dinamicamente no FROM costumam ser as grandes vilãs. É comum trazerem muitos registros sem necessidade.
  4. Caso tenha criado um indice para atender determinada consulta e o Plano de Execução não citou o uso dele no respectivo operador. Provavelmente seu índice não foi criado da melhor forma e o banco optou por não usá-lo. Neste caso, revise e tente recriá-lo invertendo a ordem das colunas (caso tenha mais de uma) e gere novamente o plano para nova analise.
  5. Sempre que possivel, o SQL Server costuma sugerir a criação ou alteração de indices.
  6. Sempre que vermos a palavra Scan acompanhada do nome de um índice, por exemplo, significa que ele esta sendo varrido na sua totalidade ou dentro de um intervalo de registros. Neste caso, a sua eficiencia não será totalmente atingida.

Outra forma de gerar o Plano

Continuando no SQL Server Managemente Studio, crie um novo script e execute individualmente o comando:

SET SHOWPLAN_ALL ON
GO

Temos varias formas de apresentação do Plano:

Formato Descrição
SHOWPLAN_TEXT Exibe um plano de execução estimado baseado em texto básico, sem executar a consulta.
SHOWPLAN_ALL Exibe um plano de execução estimado baseado em texto com estimativas de custo, sem executar a consulta.
SHOWPLAN_XML Exibe um plano de execução estimado baseado em XML com estimativas de custo, sem executar a consulta.
Isso é equivalente à opção "Exibir plano de execução estimado" no SQL Server Management Studio.
STATISTICS PROFILE Executa a consulta e exibe um plano de execução real baseado em texto.
STATISTICS XML Executa a consulta e exibe um plano de execução real baseado em XML.
Isso é equivalente à opção "Incluir plano de execução real" no SQL Server Management Studio.

Aproveitando o mesmo script, execute a consulta utilizada anteriormente para gerar o plano visual:

select a.id_a,
a.desc_a,
b.id_b,
b.desc_b
from tb_a a
join tb_b b on b.id_a = a.id_a

A partir do momento que a consulta for executada, o Plano será apresentado no formato texto, com detalhamento por operador. A imagem a seguir, apresenta parte do resultado gerado:
plano-estimado-texto-1

Os cuidados que precisamos tomar na analise deste resultado são os mesmos citados neste artigo, nos itens Analisando o que foi gerado e Pontos de atenção.

Conclusão

Neste artigo foram dadas algumas dicas importantes que aprendi nos longos anos de trabalho com instruções SQL e extração de dados. Muitas delas são reconhecidas e encontradas em artigos de profissionais da área. Tentei sintetiza-las e trazer juntamente algumas orientações sobre o uso do Plano de Execução no SQL Server. Ferramenta que eu considero a mais importante disponibilizada pelos bancos para auxilio no trabalho de otimização.
As abordagens citadas neste post, são introdutórias. O pouco visto até aqui já servem de base e fornece apoio para a maioria das analises de consultas que realizei até o momento em que escrevi o artigo.
Agradeço a sua atenção até aqui e espero que aprecie os demais artigos que falam do mesmo assunto em Postgres e Oracle escrito pelo nosso amigo João do Carmo.

Referências Bibliográficas

https://www.devmedia.com.br/sql-server-query-analise-do-plano-de-execucao/30024
https://docs.microsoft.com/pt-br/sql/relational-databases/performance/display-an-actual-execution-plan?view=sql-server-ver15
https://qastack.com.br/programming/7359702/how-do-i-obtain-a-query-execution-plan-in-sql-server