Acelere sua aplicação SQL Server
Olá, pessoal! Este é meu primeiro post no nosso Blog, e acho que cabem aqui algumas explicações. Estou na Locaweb há mais ou menos cinco anos e meio, sempre dentro da área de tecnologia, e estive aqui durante boa parte do crescimento espantoso desta que é a melhor e maior empresa de serviços de internet do Brasil, sem nenhuma pretensão e nem falsa modéstia.
Neste tempo, aprendi uma coisa ou outra sobre as ferramentas que utilizamos e oferecemos, e acho que posso me arriscar a dar umas dicas técnicas de vez em quando. Aos incautos ou tecnoalérgicos, um aviso: este post não interessa para vocês. Aos escovadores de bits e bytes: usem sem moderação. Desculpem-me pela extensão do texto, divirtam-se e, por favor, mandem feedback para eu saber se estes temas interessam. Caso contrário, posso mudar de tema e contar uns podres da festa de fim de ano.
Uma situação comum que encontramos na LocaWeb é de clientes que se frustram com aplicações utilizando MS SQL Server. Eles acham que o desempenho do servidor está ruim, já que suas aplicações se mostram lentas e ocasionalmente exibem mensagens de erro na tela para seus clientes. Em praticamente todos os casos, ao analisar o que ocorre, verificamos que as práticas adotadas na aplicação não favorecem o desempenho do banco de dados, e isto acaba gerando uma cascata de problemas que culmina com mensagens de erro nada amigáveis.
Um dos problemas mais graves é o uso de cursores. “Como assim? Que outra alternativa há para percorrer dados no SQL?”, pergunta-se o leitor. Respondo com uma frase cujo autor não poderei citar aqui, por puro esquecimento do nome dele: “Com cursor, qualquer um faz. O desafio em SQL é fazer tudo sem cursor”. Se preferirem, ao invés de falar mal dos cursores, podemos falar que o problema é a falta de uso de variáveis-tabela, que são a alternativa que apresentarei em seguida.
Cursores são uma forma fácil de percorrer dados de um conjunto no SQL. Qualquer programador domina o conceito de “loop” e é capaz de iterar sobre um conjunto de dados até chegar no último elemento. O problema é que no SQL Server isso é muito ineficiente. Funciona bem para pequenos conjuntos de dados, mas mesmo nestes a técnica que vou propor traz ganhos no longo prazo.
Suponhamos que temos uma tabela de 200.000 registros e que tenhamos que percorrer alguns dados dela (algo como 10.000) fazendo um “update” em uma segunda tabela nos registros que tiverem o mesmo valor da primeira. O caminho natural seria fazer o seguinte:
declare
c
cursor local for
select chave from tabela1 where campo = ‘valor’
declare @chave int
open c
fetch next from c into @chave
while (@@fetch_status = 0)
begin
update tabela2 set coluna = 1 where chave = @chave
fetch next from c into @chave
end
close c
deallocate c
Bem, este é o caminho do cursor. A alternativa é utilizar variáveis-tabela no lugar dos cursores. Estas variáveis funcionam exatamente como uma tabela, ou uma tabela temporária, só que são ordens de grandeza mais rápidas no uso do que um cursor. Elas têm muitos usos, e praticamente em 100% dos casos aceleram a aplicação. Abaixo coloco uma implementação do mesmo código que coloquei acima, mas usando uma variável-tabela no lugar do cursor.
declare
@t
table
(chave int)
insert into @t select chave from tabela1 where campo = ‘valor’
declare @chave int
while (select count(*) from @t) > 0
begin
select top 1 @chave = T.chave from @t T
update tabela2 set coluna = 1 where chave = @chave
delete from @t where chave = @chave
end
Este código, além de mais enxuto, é ordem de grandeza mais rápido. Quanto maior o conjunto de dados que o cursor contém, maior a diferença de desempenho.
Outro bom exemplo de aplicação de variáveis-tabela é quando tenho que fazer “joins” de muitas tabelas. É uma boa prática em SQL evitar “joins” de mais de 4 tabelas ao mesmo tempo. Mesmo com menos tabelas, ao fazer o “join” entre tabelas muito grandes a tendência é termos um desempenho muito ruim. A solução? Ao invés de fazer o SQL comparar todos os registros das tabelas (que, a grosso modo, é o que o “join” vai fazer - DBAs, não me matem, estou simplificando), você declara diversas variáveis-tabela e filtra as tabelas individualmente, fazendo os “joins” já com um conjunto menor de dados.
Vou dar um exemplo pra facilitar. Imagine que tenho 3 tabelas, cada uma com 100.000 linhas. Preciso fazer um “join” entre as 3, para pegar informações de cada uma delas. Imagine que no meu select vou retornar algo como 5 linhas apenas. Um código assim poderia ser:
select
P.nome
from
PESSOA P -- 100.000 registros
inner join
EMPRESA E -- 100.000 registros
on P.chave_empresa = E.chave
inner join
NOTA_FISCAL N -- 100.000 registros
on E.cnpj = N.cnpj
where
N.num_nota in (1,2,3,4,5)
Só me interessam 5 registros da tabela NOTA_FISCAL neste exemplo, cada um relacionado a apenas 1 empresa e apenas 1 pessoa, ou seja, só vou retornar 5 registros no total. Para isso, estou fazendo um join de 3 tabelas muito grandes, o que é bem ineficiente.
Pois bem, vou dar um exemplo com variáveis-tabela onde o desempenho é muito melhor:
declare @t1 table (cnpj NVARCHAR(20))
insert into @t1 select cnpj from NOTA_FISCAL where num_nota in (1,2,3,4,5)
declare @t2 table (chave BIGINT)
insert into @t2 select E.chave from EMPRESA E inner join @T1 T on E.cnpj = T.cnpj
/* ESTE SELECT RETORNARIA O MESMO RESULTADO DO SCRIPT QUE NAO USA VARIAVEIS-TABELA ACIMA */
select P.nome from PESSOA P inner join @t2 T on P.chave_empresa = T.chave
Não aceitem minha palavra, façam o teste. O resultado é garantido e impressionante.
Espero ter dado aqui uma primeira contribuição para quem sente que seus scripts SQL às vezes estão com o freio de mão puxado. Quem gostou, avise-me para saber se devo escrever mais. Quem não gostou, me avise também, assim na próxima dou umas dicas de cinema!




19 de abril de 2006 at 08:45
Interessante a dica de utilizar tabelas. Vou fazer alguns testes, pois tenho enfrentado alguns problemas de desempenho.
6 de maio de 2006 at 16:26
Muito bom !!!
11 de maio de 2006 at 12:37
Legal…, aguardo mais dicas.
11 de julho de 2006 at 18:05
Bastante interessante o artigo, porém para o 2o exemplo, seguindo os conceitos da álgebra relacional, poderia ter sido feito, com o mesmo ganho de performance proposto, o seguinte:
select
P.nome
from
PESSOA P — 100.000 registros
inner join
EMPRESA E — 100.000 registros
on P.chave_empresa = E.chave
inner join
NOTA_FISCAL N — 100.000 registros
on (N.num_nota in (1,2,3,4,5)) and (E.cnpj = N.cnpj)
Seria legal fazer uns testes comparando esses métodos numa segunda parte deste artigo. ;)
20 de agosto de 2006 at 02:47
Eu gostaria muito de ver uma desta dicas de cinema… tem uma base de dados muito grade, e sinceramente… está precisando de uma otimização nas buscas!!!
esse foi legal… mas já utilizo variáveis tabela.!!!
Valeu
26 de setembro de 2006 at 11:11
Bom dia Henrique Macedo,
Reproduzimos o experimento acima em duas versões do SQL server. Na versão 2000 do Sql Server os resultados obtidos são similares aos seus, contudo na versão 2005 o comportamento é o oposto, a utilização do cursor fast_forward local se apresentou cerca de 12 vezes mais eficientes para um conjunto de 500.000 elementos.
Quando houver mais novidades favor entrar em contato.
7 de dezembro de 2006 at 10:22
aeeeeeeeeeeee!!!
sou de Olinda - Pernambuco
sou estudando de Desenvolvimento de Software da UNIBRATEC (Universidade Brasileira de Tecnologia)
eu tava com um problemão com esse negócio de “Inner Join” no SQL server… aí eu vim no seu site e esclareci tudo…
quero que vc saiba que estou grandemente agradecido, viu?
valeeeu!!!
Deus te abençôe!!!! o/
10 de janeiro de 2007 at 16:55
Dica muito boa.
Obrigado !!
2 de março de 2007 at 11:43
legal !
sobre a sua pergunta entre continuar com as dicas ou contar os podres da festa, fico com as dicas !
abraço
6 de novembro de 2007 at 11:41
Bah, meu os teus exemplos de update linha a linha é muito amador. Já ouviu falar em Hash Join? já ouviu falar em update from ?
Deixa o banco trabalhar por ti.
Abraço.