O índice na coluna Persisted Computed precisa de pesquisa de chave para obter colunas na expressão computada

24

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 
);

Como isso Compnão é exclusivo, e D é a data de validade de cada combinação de A, B, C, portanto, uso a seguinte consulta para obter a data de término de cada uma A, B, C(basicamente a próxima data de início 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;

Adicionei um índice à coluna computada para ajudar nessa 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 pensaria que, como tenho uma cláusula where afirmando isso D IS NOT NULLe estou classificando por Comp, e não fazendo referência a nenhuma coluna fora do índice, o índice na coluna computada poderia ser usado para varrer t1 e t2, mas vi um índice clusterizado digitalizar.

insira a descrição da imagem aqui

Então forcei o uso desse índice para ver se ele produzia 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;

O que deu esse plano

insira a descrição da imagem aqui

Isso mostra que uma pesquisa de chave está sendo usada, cujos detalhes são:

insira a descrição da imagem aqui

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 quando cria 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 pode provar com precisão se uma função que retorna expressões de coluna calculada, 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 no meu índice, por que uma Pesquisa de Chave é necessária para obter A, B e C quando eles não são mencionados em a consulta? Suponho que eles estejam sendo usados ​​para calcular Comp, mas por quê? Além disso, por que a consulta pode usar o índice em t2, mas não em t1?

Consultas e DDL no SQL Fiddle

Nota: eu 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.

GarethD
fonte

Respostas:

20

Por que é necessária uma pesquisa de chave para obter A, B e C quando eles não são referenciados na consulta? Suponho que eles estejam sendo usados ​​para calcular Comp, mas por quê?

As colunas A, B, and C são referenciadas no plano de consulta - elas são usadas pela pesquisa T2.

Além disso, por que a consulta pode usar o índice em T2, mas não em T1?

O otimizador decidiu que a varredura do índice clusterizado era mais barata que a varredura do índice filtrado não clusterizado e, em seguida, realizando 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 do índice. Esperamos que ele leia a Compcoluna usando uma varredura de índice não clusterizada e, em seguida, execute uma busca no mesmo índice (também conhecido como T2) para localizar o registro Top 1.

O otimizador de consultas expande as referências de colunas calculadas antes do início da otimização, para que você possa avaliar 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á-lo' para um formulário que achar mais fácil de raciocinar. Se não conseguir encontrar uma simplificação mais eficaz, recorre à reescrita da subconsulta correlacionada como uma aplicação (uma junção correlacionada):

Aplicar reescrita

Acontece que esse desenrolar de aplicação coloca a árvore de consultas lógicas em um formulário 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 maneira como a consulta é gravada interage com os detalhes internos do otimizador, de forma que a definição da expressão expandida não corresponda à coluna calculada e você termina com uma busca que faz referência a colunas em A, B, and Cvez da coluna calculada Comp. Esta é a causa raiz.

Solução alternativa

Uma idéia para solucionar esse efeito colateral é escrever a consulta como uma aplicação manual:

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
CROSS APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

Infelizmente, essa consulta não usará o índice filtrado como esperávamos. O teste de desigualdade na coluna Ddentro da aplicação rejeita NULLs, portanto o predicado aparentemente redundante WHERE T1.D IS NOT NULLé otimizado.

Sem esse predicado explícito, a lógica de correspondência do índice filtrado decide que não pode usar o índice filtrado. Existem várias maneiras de contornar esse segundo efeito colateral, mas o mais fácil é provavelmente alterar a aplicação cruzada para uma aplicação externa (espelhando a lógica da reescrita do otimizador executada anteriormente na subconsulta correlacionada):

SELECT
    T1.ID,
    T1.Comp,
    T1.D,
    CA.D2
FROM dbo.T AS T1
OUTER APPLY
(  
    SELECT TOP (1)
        D2 = T2.D
    FROM dbo.T AS T2
    WHERE
        T2.Comp = T1.Comp
        AND T2.D > T1.D
    ORDER BY
        T2.D ASC
) AS CA
WHERE
    T1.D IS NOT NULL -- DON'T CARE ABOUT INACTIVE RECORDS
ORDER BY
    T1.Comp;

Agora, o otimizador não precisa usar a aplicação reescrita (para que a correspondência calculada da coluna funcione conforme o esperado) e o predicado também não seja otimizado, portanto o índice filtrado pode ser usado para ambas as operações de acesso a dados e a pesquisa usa a Compcoluna em ambos os lados:

Plano de aplicação externa

Geralmente, isso seria preferível à adição de A, B e C como INCLUDEdcolunas no índice filtrado, porque aborda a causa raiz do problema e não requer a ampliação desnecessária do índice.

Colunas computadas persistentes

Como uma observação lateral, não é necessário marcar a coluna computada como PERSISTED, se você não se importa em repetir sua definição em uma CHECKrestrição:

CREATE TABLE dbo.T 
(   
    ID integer IDENTITY(1, 1) NOT NULL,
    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,

    CONSTRAINT CK_T_Comp_NotNull
        CHECK (A + '-' + B + '-' + C IS NOT NULL),

    CONSTRAINT PK_T_ID 
        PRIMARY KEY (ID)
);

CREATE NONCLUSTERED INDEX IX_T_Comp_D
ON dbo.T (Comp, D) 
WHERE D IS NOT NULL;

É necessário que a coluna computada esteja PERSISTEDnesse caso se você desejar usar uma NOT NULLrestrição ou fazer referência Compdireta à coluna (em vez de repetir sua definição) em uma CHECKrestrição.

Paul White diz que a GoFundMonica
fonte
2
+1 BTW Encontrei outro caso de pesquisa supérflua enquanto olhava para isso que você pode (ou não) achar interessante. SQL Fiddle .
Martin Smith
@ MartinSmith Sim, isso é interessante. Outra regra genérica reescreve ( FOJNtoLSJNandLASJN) que resulta em coisas que não funcionam como seria de esperar e deixando lixo (BaseRow / Checksums) que é útil em alguns tipos de planos (por exemplo, cursores), mas não é necessário aqui.
Paul White diz GoFundMonica
Ah Chké soma de verificação! Obrigado, eu não tinha certeza disso. Originalmente, eu estava pensando que poderia ter algo a ver com restrições de verificação.
Martin Smith
6

Embora isso possa ser um pouco de coincidência devido à natureza artificial dos seus dados de teste, como você mencionou no SQL 2012, tentei reescrever:

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;

Isso resultou em um bom plano de baixo custo usando seu índice e com leituras significativamente mais baixas que as outras opções (e os mesmos resultados para seus dados de teste).

Planeje os custos do Explorer para quatro opções: Original;  original com dica;  exterior aplicar e chumbo

Eu suspeito que seus dados reais são mais complicados, portanto, pode haver alguns cenários em que essa consulta se comporta semanticamente diferente da sua, mas às vezes mostra que os novos recursos podem fazer uma diferença real.

Eu experimentei alguns dados mais variados e encontrei alguns cenários para corresponder e outros não:

--Example 1: results matched
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn + b.rn, '1 Jan 2013')
FROM cte a
    CROSS JOIN cte b
WHERE a.rn % 3 = 0
 AND b.rn % 5 = 0
ORDER BY 1, 2, 3
GO


-- Original query
SELECT  t1.ID,
        t1.Comp,
        t1.D,
        D2 = (  SELECT  TOP 1 D
                FROM    dbo.T t2
                WHERE   t2.Comp = t1.Comp
                AND     t2.D > t1.D
                ORDER BY D
            )
INTO #tmp1
FROM    dbo.T t1 
WHERE   t1.D IS NOT NULL
ORDER BY t1.Comp;
GO

SELECT  ID,
        Comp,
        D,
        D2 = LEAD(D) OVER(PARTITION BY COMP ORDER BY D)
INTO #tmp2
FROM    dbo.T 
WHERE   D IS NOT NULL
ORDER BY Comp;
GO


-- Checks ...
SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1


Example 2: results did not match
TRUNCATE TABLE dbo.t

-- Generate some more interesting test data
;WITH cte AS
(
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT T (A, B, C, D)
SELECT  'A' + CAST( a.rn AS VARCHAR(5) ),
        'B' + CAST( a.rn AS VARCHAR(5) ),
        'C' + CAST( a.rn AS VARCHAR(5) ),
        DATEADD(DAY, a.rn, '1 Jan 2013')
FROM cte a

-- Add some more data
INSERT dbo.T (A, B, C, D)
SELECT A, B, C, D 
FROM dbo.T
WHERE DAY(D) In ( 3, 7, 9 )


INSERT dbo.T (A, B, C, D)
SELECT A, B, C, DATEADD( day, 1, D )
FROM dbo.T
WHERE DAY(D) In ( 12, 13, 17 )


SELECT * FROM #tmp1
EXCEPT
SELECT * FROM #tmp2

SELECT * FROM #tmp2
EXCEPT
SELECT * FROM #tmp1

SELECT * FROM #tmp2
INTERSECT
SELECT * FROM #tmp1


select * from #tmp1
where comp = 'A2-B2-C2'

select * from #tmp2
where comp = 'A2-B2-C2'
wBob
fonte
11
Bem, ele usa o índice, mas apenas até certo ponto. Se compnão for uma coluna computada, você não verá a classificação.
Martin Smith
Obrigado. Meu cenário atual não é muito mais complicado e a LEADfunção funcionou exatamente como eu gostaria na minha instância local do 2012 express. Infelizmente, esse pequeno inconveniente para mim ainda não foi considerado um motivo suficientemente bom para atualizar os servidores de produção ...
GarethD 29/13
-1

Quando tentei executar as mesmas ações, obtive os outros resultados. Primeiro, meu plano de execução para tabela sem índices é o seguinte:insira a descrição da imagem aqui

Como podemos ver na Análise de Índice em Cluster (t2), o predicado é usado para determinar as linhas necessárias a serem retornadas (devido à condição):

insira a descrição da imagem aqui

Quando o índice foi adicionado, independentemente de ter sido definido pelo operador WITH ou não, o plano de execução passou a ser o seguinte:

insira a descrição da imagem aqui

Como podemos ver, a verificação de índice em cluster é substituída pela verificação de índice. Como vimos acima, o SQL Server usa as colunas de origem da coluna computada para executar a correspondência da consulta aninhada. Durante a varredura de índice em cluster, todos esses valores podem ser adquiridos ao mesmo tempo (nenhuma operação adicional é necessária). Quando o índice foi adicionado, a filtragem das linhas necessárias da tabela (na seleção principal) está executando de acordo com o índice, mas os valores das colunas de origem da coluna computada compainda precisam ser obtidos (última operação Nested Loop) .

insira a descrição da imagem aqui

Por esse motivo, a operação Key Lookup é usada - para obter os dados das colunas de origem da computada.

PS Parece um bug no SQL Server.

Sandr
fonte