Data

2 out, 2009

Index Seek em Where com função. Como?

Publicidade

Certa vez, em um  projeto, me deparei com uma situação bem interessante. Tínhamos um banco de um software de terceiro que controlava a frente de caixa da rede. Em determinada tabela, o collate dela era Case Sensitive.

Uma das consultas campeãs de reads era feita nela, com um ?where upper(campo) = ?.  A consulta era feita ad-hoc e, por motivos que não me cabem, não se podia (ou não tinha tempo hábil), para alterar a consulta tirando o upper.

O index scan era feito na consulta e tinha um custo altíssimo de IO (logical reads altos e physical reads também) e realmente travava a galera quando rodava, que era de 1 em 1 hora por um job e demorava 20 minutos. OU seja, 20 minutos que parava.

Como foi resolvido?

Usando uma computed column e um índice nesta computed column.

Vamos lá.

Criando o ambiente

  • Criar uma tabela com um campo Case Sensitive
create table TesteComputed (    codigo int identity Primary key Clustered,
                                               Valor1 decimal(10,2),
                                               Valor2 decimal(10,2),
                                                Nome varchar(100) Collate Latin1_General_CS_AS
                                        )
go        
  • Criar o Indice na tabela, campo Nome
create index idx_TesteComputed_Nome on TesteComputed(nome)
  • Popular a tabela
;WITH ComputedCte (Id, CteValor1, CteValor2)
AS
(
SELECT    1,
        ABS(CheckSum(NEWID()) / 1000000.8764),
        ABS(CheckSum(NEWID()) / 2000000.7652)
UNION ALL
SELECT    id+1,
        ABS(CheckSum(NEWID()) / 1000000.8764 ) ,
        ABS(CheckSum(NEWID()) / 2000000.7652)
FROM ComputedCte
where    id < 31000
)
insert into TesteComputed(Valor1,Valor2,Nome)
Select    Ctevalor1,
             Ctevalor2,
             Case
            when Ctevalor1 between  1 and 999              Then    'Campo2'
            when Ctevalor1 between  1000 and 10000     Then    'Campo1'
            when Ctevalor1 between  12000 and 20000   Then    'Campo2'
        end       
from    ComputedCte
OPTION (MAXRECURSION 31000)

Agora, vamos aos testes

Vamos ver como fica se eu montar esta consulta sem o upper:

select    nome 
from    TesteComputed
where    Nome = 'Campo1'

(16632 row(s) affected)

Table 'TesteComputed'. Scan count 1, logical reads 45, physical
reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0,
lob read-ahead reads 0.

SELECT [nome] FROM [TesteComputed] WHERE [Nome]=@1
  |--Index
Seek(OBJECT:([DBA].[dbo].[TesteComputed].[idx_TesteComputed_Nome]),
SEEK:([DBA].[dbo].[TesteComputed].[Nome]=CONVERT_IMPLICIT(varchar(8000),[@1],0))
ORDERED FORWARD)

Além do que, se eu não colocar o ‘Campo’ com a primeira letra em maiúsculo como está na tabela, não me retornará nada, pois a coluna é case sensitive.

Agora usando o upper :

select    nome
from TesteComputed
where upper(Nome) = 'CAMPO1'
(16632 row(s) affected)
Table 'TesteComputed'. Scan count 1, logical reads 81, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
select nome from TesteComputed where upper(Nome) = 'CAMPO1'
|--Index Scan(OBJECT:([DBA].[dbo].[TesteComputed].[idx_TesteComputed_Nome]), WHERE:(upper([DBA].[dbo].[TesteComputed].[Nome])='CAMPO1')

Podemos ver bem que o logical reads aumentou e, ao invés de um seek, estamos fazendo scan agora.

Vamos transformar esse scan em um seek ainda usando o upper no where:

  • Crie mais um campo na tabela. Este será um campo calculado. Ele terá o upper do campo nome:
Alter table TesteComputed add UpperNome as upper(nome)
  • Crie o índice neste campo e atualize as estatísticas com fullscan (não é necessário, mas é altamente recomendável – vai demorar um pouquinho)
create index idx_TesteComputed_uppernome on TesteComputed(uppernome)
include (nome)
go
update statistics TesteComputed with fullscan

Agora rode a mesma consulta novamente:

(16632 row(s) affected)
Table 'TesteComputed'. Scan count 1, logical
reads 60, physical reads 0, read-ahead reads 0, lob logical reads 0,
lob physical reads 0, lob read-ahead reads 0.

select nome   from TesteComputed  where upper(Nome) = 'CAMPO1'
 
|--Index
Seek(OBJECT:([DBA].[dbo].[TesteComputed].[idx_TesteComputed_uppernome]),
SEEK:([DBA].[dbo].[TesteComputed].[UpperNome]='CAMPO1') ORDERED FORWARD)

Tá aí… temos um index seek.

Bom, isso foi extrema valia para mim, pois consegui resolver sem precisar acionar o suporte da empresa numa sexta à noite.

Desta maneira, aquela consulta que demorava 20 minutos e tinha aproximadamente 800.000 logical reads e 180 physical, caiu para 130.000 reads (movimentava uma quantidade grande de linhas) e 0 physical reads.

Sem falar que caiu para um minuto e meio.

Nos próximos artigos, falarei sobre alguns outros casos de sucesso que tive usando colunas computadas, neste mesmo projeto.

Um abraço!