Eu tenho uma coluna computada persistente em uma tabela que é simplesmente composta de colunas concatenadas, por exemplo
CREATE TABLE dbo.T
(
ID INT IDENTITY(1, 1) NOT NULL CONSTRAINT PK_T_ID PRIMARY KEY,
A VARCHAR(20) NOT NULL,
B VARCHAR(20) NOT NULL,
C VARCHAR(20) NOT NULL,
D DATE NULL,
E VARCHAR(20) NULL,
Comp AS A + '-' + B + '-' + C PERSISTED NOT NULL
);
Isso Comp
não é exclusivo e D é a data de início válida de cada combinação de A, B, C
, portanto, uso a seguinte consulta para obter a data final de cada uma A, B, C
(basicamente a próxima data inicial para o mesmo valor de Comp):
SELECT t1.ID,
t1.Comp,
t1.D,
D2 = ( SELECT TOP 1 t2.D
FROM dbo.T t2
WHERE t2.Comp = t1.Comp
AND t2.D > t1.D
ORDER BY t2.D
)
FROM dbo.T t1
WHERE t1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY t1.Comp;
Em seguida, adicionei um índice à coluna computada para auxiliar nesta consulta (e também em outras):
CREATE NONCLUSTERED INDEX IX_T_Comp_D ON dbo.T (Comp, D) WHERE D IS NOT NULL;
O plano de consulta, no entanto, me surpreendeu. Eu teria pensado que, como tenho uma cláusula where informando isso D IS NOT NULL
e estou classificando por Comp
, e não fazendo referência a nenhuma coluna fora do índice, que o índice na coluna computada poderia ser usado para digitalizar t1 e t2, mas vi um índice agrupado Varredura.
Então forcei o uso desse índice para ver se dava um plano melhor:
SELECT t1.ID,
t1.Comp,
t1.D,
D2 = ( SELECT TOP 1 t2.D
FROM dbo.T t2
WHERE t2.Comp = t1.Comp
AND t2.D > t1.D
ORDER BY t2.D
)
FROM dbo.T t1 WITH (INDEX (IX_T_Comp_D))
WHERE t1.D IS NOT NULL
ORDER BY t1.Comp;
Que deu este plano
Isso mostra que uma pesquisa de chave está sendo usada, cujos detalhes são:
Agora, de acordo com a documentação do SQL-Server:
Você pode criar um índice em uma coluna computada definida com uma expressão determinística, mas imprecisa, se a coluna estiver marcada como PERSISTED na instrução CREATE TABLE ou ALTER TABLE. Isso significa que o Mecanismo de Banco de Dados armazena os valores calculados na tabela e os atualiza quando quaisquer outras colunas das quais a coluna calculada depende são atualizadas. O Mecanismo de Banco de Dados usa esses valores persistentes ao criar um índice na coluna e quando o índice é referenciado em uma consulta. Essa opção permite criar um índice em uma coluna computada quando o Mecanismo de Banco de Dados não puder provar com precisão se uma função que retorna expressões de coluna computada, particularmente uma função CLR criada no .NET Framework, é determinística e precisa.
Portanto, se, como os documentos dizem "o mecanismo de banco de dados armazena os valores calculados na tabela" , e o valor também está sendo armazenado em meu índice, por que é necessária uma pesquisa de chave para obter A, B e C quando eles não são referenciados em a consulta em tudo? Eu suponho que eles estão sendo usados para calcular o Comp, mas por quê? Além disso, por que a consulta pode usar o índice em t2
, mas não em t1
?
NB, marquei o SQL Server 2008 porque esta é a versão em que meu principal problema está, mas também recebo o mesmo comportamento em 2012.
As colunas
A, B, and C
são referenciadas no plano de consulta - elas são usadas pela busca emT2
.O otimizador decidiu que a varredura do índice clusterizado era mais barata do que a varredura do índice não clusterizado filtrado e, em seguida, realizar uma pesquisa para recuperar os valores das colunas A, B e C.
Explicação
A verdadeira questão é por que o otimizador sentiu a necessidade de recuperar A, B e C para a busca de índice. Esperamos que ele leia a
Comp
coluna usando uma varredura de índice não clusterizado e, em seguida, execute uma busca no mesmo índice (alias T2) para localizar o registro Top 1.O otimizador de consulta expande as referências de coluna computadas antes do início da otimização, para permitir que ele avalie os custos de vários planos de consulta. Para algumas consultas, expandir a definição de uma coluna computada permite que o otimizador encontre planos mais eficientes.
Quando o otimizador encontra uma subconsulta correlacionada, ele tenta 'desenrolá-la' para uma forma sobre a qual acha mais fácil raciocinar. Se não conseguir encontrar uma simplificação mais eficaz, ele recorre à reescrita da subconsulta correlacionada como um apply (uma junção correlacionada):
Acontece que esse desdobramento de aplicação coloca a árvore de consulta lógica em um formato que não funciona bem com a normalização do projeto (um estágio posterior que procura corresponder expressões gerais a colunas computadas, entre outras coisas).
No seu caso, a forma como a consulta é escrita interage com os detalhes internos do otimizador, de modo que a definição da expressão expandida não corresponda à coluna computada e você acaba com uma busca que faz referência a colunas
A, B, and C
em vez da coluna computada,Comp
. Esta é a causa raiz.Gambiarra
Uma ideia para contornar esse efeito colateral é escrever a consulta como uma aplicação manualmente:
Infelizmente, esta consulta não usará o índice filtrado como esperamos. O teste de desigualdade na coluna
D
dentro do apply rejeitaNULLs
, então o predicado aparentemente redundanteWHERE T1.D IS NOT NULL
é otimizado.Sem esse predicado explícito, a lógica de correspondência de índice filtrado decide que não pode usar o índice filtrado. Há várias maneiras de contornar esse segundo efeito colateral, mas a mais fácil é provavelmente alterar a aplicação cruzada para uma aplicação externa (espelhando a lógica da reescrita que o otimizador executou anteriormente na subconsulta correlacionada):
Agora, o otimizador não precisa usar a própria reescrita de aplicação (portanto, a correspondência de coluna calculada funciona como esperado) e o predicado também não é otimizado, portanto, o índice filtrado pode ser usado para ambas as operações de acesso a dados e a busca usa a
Comp
coluna em ambos os lados:This would generally be preferred over adding A, B, and C as
INCLUDEd
columns in the filtered index, because it addresses the root cause of the problem, and does not require widening the index unnecessarily.Persisted computed columns
As a side note, it is not necessary to mark the computed column as
PERSISTED
, if you don't mind repeating its definition in aCHECK
constraint:The computed column is only required to be
PERSISTED
in this case if you want to use aNOT NULL
constraint or to reference theComp
column directly (instead of repeating its definition) in aCHECK
constraint.Although this might be a bit of a co-incidence due to the artificial nature of your test data, being as you mentioned SQL 2012 I tried a rewrite:
Isso gerou um bom plano de baixo custo usando seu índice e com leituras significativamente mais baixas do que as outras opções (e os mesmos resultados para seus dados de teste).
Suspeito que seus dados reais sejam mais complicados, portanto, pode haver alguns cenários em que essa consulta se comporte semanticamente diferente da sua, mas mostra que às vezes os novos recursos podem fazer uma diferença real.
Eu experimentei alguns dados mais variados e encontrei alguns cenários correspondentes e outros não:
Quando tentei executar as mesmas ações, obtive os outros resultados. Em primeiro lugar, meu plano de execução para tabela sem índices é o seguinte:
Como podemos ver no Clustered Index Scan (t2), o predicado é usado para determinar as linhas necessárias a serem retornadas (por causa da condição):
Quando o índice foi adicionado, não importando se foi definido pelo operador WITH ou não, o plano de execução passou a ser o seguinte:
Como podemos ver, o Clustered Index Scan foi substituído pelo Index Scan. Como vimos acima, o SQL Server utiliza as colunas de origem da coluna computada para realizar o casamento da consulta aninhada. Durante a varredura de índice clusterizado, todos esses valores podem ser adquiridos ao mesmo tempo (sem necessidade de operações adicionais). Quando o índice foi adicionado, a filtragem das linhas necessárias da tabela (na seleção principal) está sendo executada de acordo com o índice, mas os valores das colunas de origem para a coluna computada
comp
ainda precisam ser obtidos (última operação Nested Loop) .Por causa disso, a operação Key Lookup é usada - para obter os dados das colunas de origem do calculado.
PS Parece um bug no SQL Server.