| Otimizando Consultas SQL
no ambiente SQLServer
Autores: Flavio Augusto Weber e Elaini
Simoni Angelotti
1. Introdução
Através do estudo do desempenho e otimização de consultas SQL, procura-se
a minimização do tempo de resposta do servidor de banco de dados. Essa
minimização influencia na produtividade de trabalho coletivo à medida
que não degrada o desempenho operacional dos sistemas.
Dessa forma, todo sistema que acessa um banco de dados necessita obter
as respostas às consultas em um tempo hábil, pois está em jogo não somente
a paciência do usuário e credibilidade perante o mesmo, mas também valores
monetários obtidos em transações comerciais. Por exemplo, no caso de instituições
financeiras como bancos, a lentidão do sistema pode causar a perda de
clientes e conseqüentemente seus respectivos investimentos. Outro fator
importante é que, ao se melhorar o desempenho de consultas SQL, pode-se
evitar gastos com investimentos na troca de software e/ou hardware, se
os mesmos estiverem sendo adquiridos na tentativa de se conseguir melhor
desempenho do sistema.
Quando se estuda o aprimoramento de desempenho de consultas SQL, percebe-se
que, mesmo alterando ajustes de configuração e/ou incluindo hardware mais
potente, as mudanças com relação ao aplicativo freqüentemente surtem maiores
efeitos de desempenho junto às consultas. Os sistemas de banco de dados,
sejam de qualquer plataforma, podem ser extraordinariamente rápidos e
eficientes com aplicativos bem planejados e implementados. Em contrapartida,
num sistema onde os aplicativos estejam mal planejados ou implementados
de forma deficiente, o banco de dados terá um desempenho abaixo do esperado.
A chave para se obter um aplicativo bem projetado e implementado é considerar
o fator desempenho por todo o ciclo de desenvolvimento do mesmo e não
considerá-lo apenas ao término da construção para então realizar os testes
de desempenho. O desenvolvedor precisa considerar o desempenho antes de
começar a escrever o código do aplicativo.
Hardware e software apropriados, banco de dados normalizado, e principalmente
a forma com que as consultas SQL são construídas, são itens que colaboram
para a melhora do desempenho de um sistema de banco de dados.
Em alguns SGBD, existe a presença de um otimizador, como é o caso do
SQLServer. O Otimizador tem como objetivo determinar um modo eficiente
de implementar a requisição feita através de uma consulta SQL ao banco
de dados.
2. Otimização de consultas
O processamento de consultas, segundo SILBERSCHATZ et al. (1999), é uma
atividade que permite extrair dados de um banco de dados. Esta atividade
inclui a tradução de consultas expressas em linguagens de alto nível do
banco de dados em expressões que podem ser implementadas no nível físico
do sistema de arquivos, otimizações, traduções e avaliação das consultas.
O custo do processamento de uma consulta é determinado pelo acesso ao
disco. Geralmente, há diversas estratégias possíveis para processar uma
determinada consulta, principalmente se ela for complexa. A diferença
entre uma estratégia boa e uma ruim, em termos do número de acessos de
disco exigidos é freqüentemente significativa e pode ser de grande magnitude.
Conseqüentemente, vale a pena gastar uma quantia significativa de tempo
na seleção de uma estratégia boa para processar uma consulta.
Conforme DATE (2000), a otimização representa ao mesmo tempo um desafio,
visto que, a otimização é uma exigência para os sistemas relacionais quando
se espera um desempenho que atinja níveis pré-determinados, e uma oportunidade,
já que a otimização é favorecida pelo fato das expressões relacionais
estarem em um nível semântico adequado para que seja possível essa otimização.
Para que o banco de dados apresente um bom desempenho é preciso partir
do pressuposto da existência de uma boa estrutura de banco de dados, adequadamente
normalizada e que possua índices úteis. Além disso, é preciso escrever
consultas de forma otimizada. No entanto, deve-se enfatizar a importância
de se conhecer como o otimizador de consultas funciona para melhor formular
uma consulta ou para entender quais índices podem ser criados. Deve-se
escrever as consultas da maneira mais intuitiva e tentar otimizá-las apenas
se seu desempenho não parecer suficientemente bom (SOUKUP & DELANEY,
1999).
Para cada tabela envolvida na consulta SQL, o otimizador do SQLServer
avalia os argumentos de pesquisa e avalia até que ponto o índice pode
excluir linhas de uma seleção. Quanto mais linhas puderem ser excluídas,
menor será o número de linhas a serem processadas.
Entretanto, nem sempre o otimizador realiza com êxito seu propósito pelo
fato das consultas não terem sido escritas de forma eficiente. Por este
motivo, deve-se conhecer as três fases que o otimizador realiza durante
a execução de um consulta. A seguir, serão descritas essas três fases.
2.1 Análise da consulta
Nesta primeira fase da otimização, conforme SOUKUP & DELANEY (1999),
o otimizador examina cada cláusula da consulta e determina se ela pode
ser útil na limitação do volume de dados que devam ser percorridos, ou
seja, se a cláusula é útil como argumento de pesquisa (SARG) ou
como parte dos critérios de união.
Uma cláusula utilizada como argumento de pesquisa é referida como sargable
ou otimizável e pode fazer uso de um índice para recuperação mais rápida.
Uma SARG limita uma pesquisa porque especifica uma correspondência
exata, um intervalo de valores ou uma conjunção de dois ou mais itens
unidos pela função lógica AND.
A utilização da cláusula SARG pode fazer uso de um índice para
a recuperação mais rápida de uma informação. Entretanto, uma expressão
que não é sargable não pode limitar a pesquisa, portanto, um índice
não é útil para essas expressões.
2.2 Seleção do índice
Na segunda fase, o otimizador de consultas determina se existe um índice
para cada cláusula sargable, avalia a utilidade do índice determinando
a seletividade da cláusula e estima o custo para encontrar as linhas qualificadas.
A seletividade é a porcentagem expressa através da fórmula abaixo (Soukup
e Delaney, 1999):

Se a relação entre o número de registros qualificados e o total de registros
for baixa, o índice é altamente seletivo e útil, caso contrário o índice
tem pouca seletividade e não é útil. O índice será útil quando a relação
é de 5% ou menos. Caso o índice tenha uma seletividade maior de 5%, provavelmente
não será utilizado, ou ainda um outro índice será escolhido ou a tabela
será percorrida através de uma varredura completa nos dados.
Um índice é potencialmente útil se sua primeira coluna é usada no argumento
de pesquisa e esse argumento estabelece um limite inferior, um limite
superior ou ambos, para limitar a pesquisa. Além disso, se um índice contiver
cada coluna referenciada em uma consulta, mesmo que nenhuma dessas colunas
seja a primeira do índice, este índice é considerado útil.
A criação e utilização de índices são tarefas muito importantes para
a obtenção de um bom desempenho, pois os índices podem acelerar substancialmente
a recuperação e a seleção de dados.
Soukup e Delaney (1999) citam os dois tipos de índices suportados pelo
SQL Server: índices setorizados (ou agrupados) e índices não setorizados
(ou não agrupados). Os dois tipos de índices têm como base a árvore B
(balanced) - que significa árvore balanceada. A árvore B propicia
rápido acesso aos dados pesquisando em um valor-chave do índice e, pelo
fato de ser balanceada, localiza qualquer registro exigindo aproximadamente
o mesmo volume de recursos, sendo a velocidade de recuperação coerente,
pois o índice tem o mesmo alcance por toda a parte.
A figura 1 exibe a árvore padrão B, a figura 2 exibe a árvore B para
um índice setorizado e a 3 a árvore B para um índice não setorizado.

Figura 1 - Árvore B padrão para um índice do SQL Server.

Figura 2 - Árvore B para um índice setorizado.

Figura 3 - Árvore B para um índice não setorizado.
2.3. Seleção da união
Nesta terceira fase da otimização de consultas, se a consulta envolve
várias tabelas ou é uma auto-união, o otimizador de consultas avalia a
seleção de união e seleciona a estratégia de união com o menor custo.
O otimizador pode utilizar três estratégias para processar uniões: uniões
de loop aninhado, uniões de mesclagem e uniões por hash.
- União de loop aninhado: Neste tipo de união são processadas
um conjunto de loops que extraem uma linha da primeira tabela e usam essa
linha para percorrer a tabela mais interna e assim por diante, até que
o resultado que satisfaça seja utilizado para percorrer a última tabela.
O número de iterações através de qualquer um dos loops é igual ao número
de varreduras que devem ser realizadas. O conjunto de resultados é reduzido
à medida que ele avança de uma tabela para outra dentro de cada iteração
no loop.
- União de mesclagem: Pode-se utilizar uma união de mesclagem
quando nas duas tabelas a serem unidas existirem índices setorizados na
coluna de união. Geralmente, o otimizador escolhe a estratégia de união
de mesclagem quando as duas entradas de união (as tabelas a serem unidas)
já estão classificadas na coluna união.
- União por hash: A união por hash pode ser utilizada quando não
existe nenhum índice útil na coluna de união em nenhuma das entradas.
O hash permite determinar se um item de dados em particular corresponde
a um valor já existente, dividindo os dados existentes em grupos baseados
em alguma propriedade. Os dados com o mesmo valor são colocados num hash
bucket. Para verificar se um novo valor possui um correspondente nos
dados existentes, simplesmente examina-se o bucket quanto ao valor
correto.
3. Experimentos computacionais e resultados
Com o objetivo de demonstrar a execução e monitoramento de consultas
SQL, bem como comprovar a otimização obtida através das técnicas estudadas,
foram necessárias várias implementações de consultas SQL. As consultas
foram construídas de maneiras diferentes, com o intuito de demonstrar
a diferença de desempenho entre uma e outra forma de implementação (WEBER,
2002).
Para auxiliar o monitoramento destas consultas, foram utilizadas as opções
STATISTICS IO e SHOWPLAN que fazem parte da ferramenta Query
Analyzer do SQLServer.
É importante salientar o ambiente onde estes testes de validações das
consultas foram realizados, pois os tempos de execuções das consultas
podem variar substancialmente de um ambiente para outro. Para esse trabalho,
o ambiente é composto de SGBD SQL Server 7.0, sistema operacional
Windows ME, sendo executado em um computador com processador Pentium
III, 1Ghz e 128 RAM.
Após cada consulta, houve a desconexão do banco de dados para que os
dados retornados não permanecessem em memória cache, impedindo, dessa
forma, a execução mais rápida da próxima consulta a ser realizada.
3.1. Banco de Dados Utilizado - PUBS
- O banco de dados utilizado para implementação das consultas SQL está
baseado no banco de dados PUBS que acompanha o software SQL Server.
A figura 4 abaixo, exibe o Modelo Entidade-Relacionamento do banco de
dados PUBS:

Figura 4 - Diagrama Entidade-Relacionamento do banco de
dados PUBS
3.2. Resultados Obtidos
A seguir serão apresentados algumas técnicas que foram implementadas
neste trabalho e os resultados com relação a utilização dessas técnicas
de modo a melhorar o desempenho de execução das consultas.
Cláusula Where
De acordo com os testes efetuados e confirmando o que PLEW & STEPHENS
(2000) afirmam, deve-se utilizar, sempre que possível, a cláusula WHERE
para limitar o número de linhas retornadas pela consulta. Isto porque
a utilização da cláusula WHERE evita que o SQL Server realize uma
varredura total na tabela, impedindo, assim, tempo de processamento desnecessário.
Select*
Não deve-se usar SELECT * para retornar todos os atributos se
for necessário apenas dados de alguns atributos, conforme explicitado
por McGEHEE (2000).
A busca por todos atributos produz um trabalho adicional, já que será
necessário ler a página de dados de cada linha para obter os valores dos
atributos que não fazem parte do índice, caso o mesmo exista.
Cláusulas Sargable
Não deve-se utilizar cláusulas WHERE que não sejam sargable.
Cláusulas não-sargable podem evitar que um índice seja aproveitado
para melhorar o desempenho da consulta, pois essas cláusulas não limitam
a pesquisa, ou seja, toda linha deve ser avaliada(SOUKUP & DELANEY,
1999).
Através dos testes realizados foi possível confirmar a veracidade dos
fatos descritos acima.
Índice Setorizado
SOUKUP & DELANEY (1999) afirmam que é importante usar índice setorizado
em consultas delimitadas por intervalo de valores.
Isso ocorre porque quando usamos um índice setorizado em consultas delimitadas
por intervalo de valores ele mantém os dados fisicamente ordenados em
uma tabela em relação à chave, facilitando assim a busca por um intervalo
determinado de dados.
Índice Não-Setorizado
SOUKUP & DELANEY (1999) propõem a utilização de um índice não-setorizado
em consultas que sejam altamente seletiva (seletividade <= 5%).
Nos testes realizados percebeu-se que a recuperação de dados usando índice
não-setorizado envolve várias leituras para determinar os dados a serem
retornados. Se esta consulta for altamente seletiva o número de leituras
será menor e será menor também o tempo de execução dessa consulta.
Índice de Cobertura
Em consultas onde os atributos a serem retornados sejam fixos, é muito
útil a inclusão destes como parte da chave do índice não-setorizado. Com
isso, não há necessidade de leituras adicionais para obtenção dos outros
atributos mencionados na consulta, confirmando o que SOUKUP & DELANEY
(1999) afirmaram.
Cláusula Order By
PLEW & STEPHENS (2000) sugerem que a cláusula ORDER BY deve
ser evitada, a menos que seja realmente necessário.
Nos testes realizados usando esta a cláusula supracitada percebeu-se
que ela representa um overhead adicional na consulta, degradando
o desempenho da mesma.
Ordem dos atributos do índice
Os testes realizados confirmam o que SOUKUP & DELANEY (1999) afirmaram
sobre ordem dos atributos no índice. Esta ordem deve ser observada, pois
o índice só é útil se os critérios da consulta correspondem aos atributos
localizados mais à esquerda na chave de índice.
Um exemplo disso é uma lista telefônica, ou seja, ela está organizada
em ordem alfabética composta de sobrenome e nome. Para localizar um número
conhecendo-se o sobrenome basta seguir a ordem alfabética para encontrar
o número desejado. No caso de tentar localizar o número conhecendo-se
apenas o nome, será necessário percorrer a lista, pois o nome poderá estar
em qualquer página da lista.
Ordem das tabelas em um junção
Conforme afirma PLEW & STEPHENS (2000), dependendo como o otimizador
analisa a consulta, a ordem das tabelas na cláusula FROM pode fazer
diferença, listando as tabelas menores primeiro. Isto porque, avaliando
as tabelas menores em primeiro lugar, um subconjunto menor de dados é
retornado, reduzindo os overheads da consulta.
Analisando os resultados obtidos pelas consultas, verificou-se que, independente
da ordem das tabelas em uma junção, o desempenho foi idêntico, ou seja,
o otimizador do SQL Server adota a mesma estratégia, indiferentemente
da forma em que são posicionadas as tabelas na cláusula FROM.
Condição mais restritiva em uma junção
Segundo PLEW & STEPHENS (2000), as condições mais restritivas da
cláusula WHERE devem ser avaliadas em primeiro lugar. Dessa forma,
um subconjunto menor de dados é retornado, reduzindo os overheads
da consulta.
Ao analisar os resultados obtidos pelas consultas, verificou-se que,
independente da ordem da condição mais restritiva, o desempenho foi idêntico,
ou seja, o otimizador do SQL Server adota a mesma estratégia, indiferentemente
da forma em que são posicionadas as condições de restrição na cláusula
WHERE. >
Ansi Join X Join SQL-92
Na tentativa de verificar se há diferença de desempenho ao utilizar duas
consultas que retornam os mesmos dados mas que são escritas de forma diferente
(ANSI Join e Join SQL-92), pode-se concluir que ambas as
consultas são equivalentes.
Confirmando SOUKUP & DELANEY(1999), constatou-se que é gerado o mesmo
plano de execução através do mecanismo empregado pelo otimizador do SQL
Server para ambas as consultas, resultando em tempos de execução iguais.
Predicado IN X Operador OR
Segundo PLEW & STEPHENS (2000), deve-se evitar o uso do operador
OR e no seu lugar utilizar o predicado IN, aumentado assim o desempenho
da consulta.
Pode-se constatar através dos testes efetuados que o otimizador do SQL
Server avalia e executa tanto o operador OR como o predicado IN da mesma
forma e com o mesmo tempo de execução.
Dessa forma, a afirmação de PLEW & STEPHENS não tem comprovação
neste ambiente.
Cláusula Group By
Deve-se avaliar com cuidado a utilização da cláusula GROUP BY (PLEW
& STEPHENS, 2000).
A utilização da cláusula GROUP BY envolve grandes operações de
classificação, fazendo com que o tempo de resposta da consulta seja prejudicado.
Cláusula Having
PLEW & STEPHENS (2000) afirmam que a utilização da cláusula HAVING
deve ser evitada, pois acreditam que a mesma provoca um overhead
adicional, aumentando o tempo de execução da pesquisa.
Foi constatado, através da execução das consultas, que tanto quando se
utiliza a cláusula HAVING como quando não se utiliza, o desempenho
é igual para ambas consultas, inclusive retornando o mesmo plano de execução.
Stored Procedures
A utilização de stored procedures (procedimentos armazenados)
melhora o desempenho de consultas em um ambiente de rede, visto que os
mesmos já estão pré-compilados e contém um plano de execução guardado
na memória (GUNDERLOY & JORDEN, 2001).
Pelo fato das consultas não terem sido executadas em um ambiente de rede,
no qual o tempo gasto para recompilar a consulta não armazenada não se
mostra evidente, os resultados foram os mesmos para as consultas analisadas.
Seleção de União
O otimizador, geralmente, adota a melhor estratégia de processar uniões
(loop aninhado, mesclagem ou hash) em uma consulta. Entretanto,
pode-se sugerir que o mesmo adote uma estratégia diferente daquela escolhida
por ele.
Com base nas execuções das consultas analisadas, ficou evidente o fato
de que o otimizador nem sempre escolhe a melhor estratégia de união. Isto
porque, na primeira consulta, onde o otimizador escolhe a estratégia de
união, o tempo de execução ficou em 11 segundos mais lento que o melhor
tempo de resposta que é de 5s, obtido através da estratégia de união por
hash para esta consulta em particular.
Isso ocorreu porque através da utilização da estratégia de união por
hash, os acessos às tabelas envolvidas nas leituras são reduzidas
a apenas um acesso, enquanto que tanto na estratégia traçada pelo otimizador
como na estratégia de união de loop aninhado, os acessos às tabelas são
superiores, pois ambas estratégias realizam várias iterações que extraem
uma linha da primeira tabela e a utiliza para percorrer a tabela mais
interna e assim por diante, acarretando num tempo maior de execução.
Deve-se portanto, testar todas as estratégias de seleção de união e não
deixar isso por conta do otimizador.
Cláusula Distinct
A cláusula DISTINCT provoca um overhead adicional na consulta,
conseqüentemente degrada o desempenho da mesma. Por isso, deve-se utilizá-la
com extremo cuidado, apenas em casos onde realmente seja necessário. McGEHEE,
2000)
Join X Subconsulta
Se puder escolher entre a utilização de uma JOIN ou de uma subconsulta,
é interessante testar os dois métodos, pois o desempenho de cada um desses
métodos pode variar para determinada consulta.
Cláusula Not In X Cláusulas Not Exists / Left Outer
Join
A utilização da cláusula NOT IN numa consulta acarreta um loop
aninhado, resultando em um baixo desempenho da consulta. Por isso, deve-se
utilizar em seu lugar uma das seguintes cláusulas: NOT EXISTS ou
LEFT OUTER JOIN checando a condição NULL, conforme afirma
McGEHEE (2000).
Abaixo, na tabela 1, um quadro comparativo das técnicas de consultas
SQL analisadas:

Tabela 1 - Tabela de técnicas de otimização.
4. Conclusão e trabalhos futuros
A grande maioria das técnicas de otimização aqui apresentadas, propostas
por seus respectivos autores, foi devidamente comprovada como eficaz e
reduziu significativamente o tempo de execução da consulta, contribuindo,
assim, positivamente para o desempenho da consulta.
No entanto, houve técnicas de otimização que não se comportaram conforme
previsto por seus respectivos autores, isto é, não corresponderam à expectativa
de redução do tempo empregado para a execução da consulta.
Também foram apresentadas técnicas onde os tempos de execuções iguais
obtidos comprovaram as afirmações feitas por seus autores, ou seja, técnicas
de otimizações equivalentes entre si.
A discordância dos desempenhos das técnicas descritas por seus respectivos
autores, diante do que se obteve na prática, se deve à atuação do otimizador
do SQLServer. Por isso é de fundamental importância conhecer o funcionamento
do mesmo.
5. Referências
1. DATE, C. J. Introdução a sistemas de banco de
dados. 7. ed. Rio de Janeiro: Campus, 2000.
2.GUNDERLOY, M.; JORDEN, J. L. Dominando o SQL
Server 2000. São Paulo : Makron Books, 2001.
3. MCGEHEE, B. M. Transact-SQL SQL server performance tuning tips. 2000.
Disponível em: <http://www.sql-server-performance.com/transact_sql.asp>.
Acesso em: 24 maio 2002.
4.PLEW, R. R.; STEPHENS, R. K. Aprenda em 24 horas SQL. 2. ed. Rio de
Janeiro : Campus, 2000.
5. SILBERSCHATZ, A.; KORTH, H. F.; SUDARSHAN, S. Sistemas
de banco de dados.3. ed. São Paulo: Makron Books, 1999.
6. SOUKUP, R.; DELANEY, K. Desvendando o Microsoft
SQL Server 7.0. Rio de Janeiro: Campus, 1999.
7.WEBER, F. A. Análise de desempenho e otimização
de consultas SQL. Curitiba, 2002. 136 f. Monografia (Graduação
em Tecnologia e Processamento de Dados) - Faculdade de Ciências Exatas
e Tecnologia, Universidade Tuiuti do Paraná.
Nota:
Este trabalho foi desenvolvido como uma monografia de trabalho final para
obtenção de conclusão do curso de Tecnologia em Processamento de Dados
da Universidade Tuiuti do Paraná, além de servir como base para a publicação
do artigo "Otimização de Consultas" apresentado no Congresso
Nacional de Tecnologia da Informação e Comunicação - Sucesu2003.
flaviow@celepar.gov.br
elaini.angelotti@utp.br

|