Não foi possível criar um índice filtrado em uma coluna computada

17

Em uma pergunta anterior, é uma boa idéia desativar a escalação de bloqueios enquanto adiciona novas colunas calculadas a uma tabela? , Estou criando uma coluna computada:

ALTER TABLE dbo.tblBGiftVoucherItem
ADD isUsGift AS CAST
(
    ISNULL(
        CASE WHEN sintMarketID = 2 
            AND strType = 'CARD'
            AND strTier1 LIKE 'GG%' 
        THEN 1 
        ELSE 0 
        END
    , 0) 
    AS BIT
) PERSISTED;

A coluna calculada é PERSISTEDe, de acordo com a computed_column_definition (Transact-SQL) :

PERSISTIDO

Especifica que o Mecanismo de Banco de Dados armazenará fisicamente os valores calculados na tabela e atualizará os valores quando quaisquer outras colunas das quais a coluna calculada depende forem atualizadas. Marcar uma coluna computada como PERSISTED permite que um índice seja criado em uma coluna calculada que é determinística, mas não precisa. Para mais informações, consulte Índices em colunas computadas. Quaisquer colunas computadas usadas como colunas de particionamento de uma tabela particionada devem ser explicitamente marcadas como PERSISTED. computed_column_expression deve ser determinístico quando PERSISTED for especificado.

Mas quando tento criar um índice na minha coluna, recebo o seguinte erro:

CREATE INDEX FIX_tblBGiftVoucherItem_incl
ON dbo.tblBGiftVoucherItem (strItemNo) 
INCLUDE (strTier3)
WHERE isUsGift = 1;

O índice filtrado 'FIX_tblBGiftVoucherItem_incl' não pode ser criado na tabela 'dbo.tblBGiftVoucherItem' porque a coluna 'isUsGift' na expressão de filtro é uma coluna calculada. Reescreva a expressão de filtro para que ela não inclua esta coluna.

Como posso criar um índice filtrado em uma coluna computada?

ou

Existe uma solução alternativa?

Marcello Miorelli
fonte
3
Você pode criar um índice filtrado no WHERE (sintMarketID = 2 AND strType = 'CARD' AND strTier1 LIKE 'GG%')entanto.
precisa saber é o seguinte

Respostas:

20

Infelizmente, no SQL Server 2014, não há capacidade de criar um Filtered Indexlocal em que o filtro esteja em uma coluna computada (independentemente de sua persistência ou não).

Há um item do Connect aberto desde 2009, então vá em frente e vote nele. Talvez a Microsoft conserte isso um dia.

Aaron Bertrand tem um artigo que cobre uma série de outras questões com índices filtrados .

Mark Sinkinson
fonte
21

Embora você não possa criar um índice filtrado em uma coluna persistente, há uma solução bastante simples que você pode usar.

Como teste, criei uma tabela simples com uma IDENTITYcoluna e uma coluna computada persistente com base na coluna de identidade:

USE tempdb;

CREATE TABLE dbo.PersistedViewTest
(
    PersistedViewTest_ID INT NOT NULL
        CONSTRAINT PK_PersistedViewTest
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , SomeData VARCHAR(2000) NOT NULL
    , TestComputedColumn AS (PersistedViewTest_ID - 1) PERSISTED
);
GO

Em seguida, criei uma exibição vinculada ao esquema com base na tabela com um filtro na coluna computada:

CREATE VIEW dbo.PersistedViewTest_View
WITH SCHEMABINDING
AS
SELECT PersistedViewTest_ID
    , SomeData 
    , TestComputedColumn
FROM dbo.PersistedViewTest
WHERE TestComputedColumn < CONVERT(INT, 27);

Em seguida, criei um índice em cluster na visualização ligada ao esquema, que tem o efeito de persistir os valores armazenados na visualização, incluindo o valor da coluna computada:

CREATE UNIQUE CLUSTERED INDEX IX_PersistedViewTest
ON dbo.PersistedViewTest_View(PersistedViewTest_ID);
GO

Insira alguns dados de teste na tabela:

INSERT INTO dbo.PersistedViewTest (SomeData)
SELECT o.name + o1.name + o2.name
FROM sys.objects o
    CROSS JOIN sys.objects o1
    CROSS JOIN sys.objects o2;

Crie um item de estatística e um índice na exibição:

CREATE STATISTICS ST_PersistedViewTest_View
ON dbo.PersistedViewTest_View(TestComputedColumn)
WITH FULLSCAN;

CREATE INDEX IX_PersistedViewTest_View_TestComputedColumn
ON dbo.PersistedViewTest_View(TestComputedColumn);

A execução de SELECTinstruções na tabela com a coluna persistente agora pode usar automaticamente a exibição persistida, se o otimizador de consultas determinar que faz sentido fazer isso:

SELECT pv.PersistedViewTest_ID
    , pv.TestComputedColumn
FROM dbo.PersistedViewTest pv
WHERE pv.TestComputedColumn = CONVERT(INT, 26)

O plano de execução real da consulta acima mostra que o otimizador de consulta optou por usar a exibição persistente para retornar os resultados:

insira a descrição da imagem aqui

Você deve ter notado a conversão explícita na WHEREcláusula acima. Esse explícito CONVERT(INT, 26)permite que o otimizador de consulta use corretamente o objeto de estatísticas para estimar o número de linhas que serão retornadas pela consulta. Se escrevermos a consulta com WHERE pv.TestComputedColumn = 26, o otimizador de consulta poderá não estimar adequadamente o número de linhas, pois 26 é realmente considerado um TINY INT; isso pode fazer com que o SQL Server não use a exibição persistente. Conversões implícitas podem ser muito dolorosas e vale a pena usar de maneira consistente os tipos de dados corretos para comparações e junções.

Obviamente, todas as "pegadinhas" padrão resultantes do uso de ligação de esquema se aplicam ao cenário acima; isso pode impedir o uso dessa solução alternativa em todos os cenários. Por exemplo, não será mais possível modificar a tabela base sem primeiro remover a ligação do esquema da visualização. Para fazer isso, você precisará remover o índice clusterizado da exibição.

Se você não possui o SQL Server Enterprise Edition, o otimizador de consulta não usará automaticamente a exibição persistente para consultas que não fazem referência direta à exibição usando a WITH (NOEXPAND)dica. Para obter o benefício de usar a exibição persistente em versões que não são da Enterprise Edition, será necessário reescrever a consulta acima para algo como:

SELECT pv.PersistedViewTest_ID
    , pv.TestComputedColumn
FROM dbo.PersistedViewTest_View pv WITH (NOEXPAND)
WHERE pv.TestComputedColumn = CONVERT(INT, 26)

Agradecemos a Ian Ringrose por apontar a limitação da Enterprise Edition acima e a Paul White pela (NOEXPAND)dica.

Esta resposta de Paul tem alguns detalhes interessantes sobre o otimizador de consultas em relação às visualizações persistentes.

Max Vernon
fonte
A solução alternativa mostra que um índice clusterizado e um índice não clusterizado são criados na exibição. O índice não clusterizado precisa ser usado sobre o índice clusterizado por algum motivo? Ou, o índice não clusterizado é mais eficiente? Se o índice clusterizado fosse usado na consulta, o que as estatísticas mostrariam?
Bob Bryan
Pergunta interessante, @BobBryan - o índice clusterizado é necessário para permitir que a exibição seja mantida, embora na verdade não precise ser um índice exclusivo. Eu poderia ter criado o índice clusterizado da exibição em alguma outra coluna, como a TestComputedColumnalternativa. No entanto, como o índice em cluster contém todos os dados da tabela / exibição, decidi que seria melhor usar um número monotonicamente crescente como a chave de cluster. Observe, na verdade, eu não testei essa suposição e, de fato, pode estar incorreta para algumas variações da reprodução.
Max Vernon
Observe que o índice não agrupado em cluster não é um índice de cobertura e, como tal, qualquer consulta que filtra, ingressa ou retorna colunas da exibição ou da tabela subjacente precisará executar uma operação de pesquisa de chave na tabela base ou a vista. É provável que, para um cenário do mundo real, o escopo limitado da minha resposta possa ser exposto com desempenho ainda melhor em mente.
Max Vernon
4

De Create Indexe sua wherecláusula, isso não é possível:

ONDE

Cria um índice filtrado especificando quais linhas incluir no índice. O índice filtrado deve ser um índice não clusterizado em uma tabela. Cria estatísticas filtradas para as linhas de dados no índice filtrado.

O predicado de filtro usa lógica de comparação simples e não pode fazer referência a uma coluna computada, uma coluna UDT, uma coluna de tipo de dados espaciais ou uma coluna de tipo de dados hierarchyID. Comparações usando literais NULL não são permitidas com os operadores de comparação. Use os operadores IS NULL e IS NOT NULL.

Fonte: MSDN

Julien Vavasseur
fonte
3
  • Você precisa de uma coluna que não seja calculada para colocar o índice filtrado.
  • Você precisa calcular o valor para ir nessa coluna.

Antes de calcularmos as colunas, usamos gatilhos para calcular o valor das colunas sempre que a linha era alterada ou inserida.

(Um gatilho também pode ser usado para inserir / remover a PK do item de uma segunda tabela que foi usada nas consultas.)

Ian Ringrose
fonte
3

Esta é uma tentativa de melhorar o trabalho de Max Vernon . Em sua solução, ele sugere usar 2 índices na exibição e um objeto de estatística.

O 1º índice é agrupado, o que é realmente necessário, pois, diferentemente de um índice não clusterizado em uma tabela, um erro será gerado se a criação de um índice não clusterizado na exibição for tentada sem primeiro ter um índice clusterizado.

O segundo índice é um índice não clusterizado, usado como o índice por trás da consulta. Na seção de comentários de sua resposta, perguntei o que aconteceria se um índice clusterizado fosse usado em vez de um índice não clusterizado.

A análise a seguir tenta responder a essa pergunta.

Estou usando exatamente o mesmo código, exceto que não estou criando um índice não clusterizado na exibição.

Também não estou criando um objeto de estatística. Se estiver acompanhando e usando o SSMS (SQL Server Management Studio) para inserir o código abaixo, você deve estar ciente de que poderá ver algumas linhas onduladas vermelhas - que parecem erros. Estes (provavelmente) não são erros, mas envolvem um problema com o intellisense.

Você pode desativar o intellisense ou simplesmente ignorar os erros e executar os comandos. Eles devem ser concluídos sem erros.

-- Create the test table that uses a computed column.
USE tempdb;
CREATE TABLE dbo.PersistedViewTest
(
    PersistedViewTest_ID INT NOT NULL
    CONSTRAINT PK_PersistedViewTest
    PRIMARY KEY CLUSTERED
    IDENTITY(1,1)
    , SomeData VARCHAR(2000) NOT NULL
    , TestComputedColumn AS (PersistedViewTest_ID - 1) PERSISTED
);
GO

-- Insert some test data into the table.
INSERT INTO dbo.PersistedViewTest (SomeData)
SELECT o.name + o1.name + o2.name
FROM sys.objects o
    CROSS JOIN sys.objects o1
    CROSS JOIN sys.objects o2;
GO

O seguinte plano de execução (sem exibição / exibição de índice) é produzido depois que a seguinte consulta é executada na tabela:

SELECT pv.PersistedViewTest_ID, pv.TestComputedColumn
FROM dbo.PersistedViewTest pv
WHERE pv.TestComputedColumn = CONVERT(INT, 26)
GO

insira a descrição da imagem aqui

Isso fornece uma linha de base para comparação. Observe que, após a consulta concluída, um objeto estatístico foi criado (_WA_Sys_00000003_1FCDBCEB). O objeto de estatísticas PK_PersistedViewTest foi criado quando o índice da tabela em cluster foi criado.

Em seguida, a visualização filtrada e o índice clusterizado nessa visualização são criados:

-- Create filtered view on the computed column.
CREATE VIEW dbo.PersistedViewTest_View
WITH SCHEMABINDING
AS
SELECT PersistedViewTest_ID, SomeData, TestComputedColumn
FROM dbo.PersistedViewTest
WHERE TestComputedColumn < CONVERT(INT, 27);
GO

-- Create unique clustered index to persist the values, including the computed column.
CREATE UNIQUE CLUSTERED INDEX IX_PersistedViewTest
ON dbo.PersistedViewTest_View(PersistedViewTest_ID);
GO

Agora, vamos tentar executar a consulta novamente, mas desta vez na visualização:

SELECT pv.PersistedViewTest_ID, pv.TestComputedColumn
FROM dbo.PersistedViewTest_View pv
WHERE pv.TestComputedColumn = CONVERT(INT, 26)
GO

O novo plano de execução é agora:

insira a descrição da imagem aqui

Se o novo plano deve ser acreditado, após a adição da visualização e do índice clusterizado nessa visualização, as estatísticas parecem indicar que o tempo necessário para executar a consulta agora dobrou. Além disso, observe que nenhum novo objeto de estatística foi criado para oferecer suporte ao novo índice após a execução da consulta, que é diferente da consulta na tabela.

O plano de consulta ainda sugere que a criação de um índice não clusterizado seria bastante útil para melhorar o desempenho da consulta. Então, isso significa que um índice não clusterizado deve ser adicionado à visualização antes que a melhoria de desempenho desejada possa ser obtida? Há uma última coisa a tentar. Modifique a consulta para usar a opção "WITH NOEXPAND":

SELECT pv.PersistedViewTest_ID, pv.TestComputedColumn
FROM dbo.PersistedViewTest_View pv WITH (NOEXPAND)
WHERE pv.TestComputedColumn = CONVERT(INT, 26)
GO

Isso resulta no seguinte plano de consulta:

insira a descrição da imagem aqui

Esse plano de execução é bastante semelhante ao produzido com o índice não clusterizado, fornecido na resposta de Max Vernon. Mas, este é feito com um índice a menos (não clusterizado) e um objeto a menos de estatística.

Acontece que a opção NOEXPAND deve ser usada com as versões expressa e padrão do SQL Server para fazer uso adequado de uma exibição indexada. Paul White tem um excelente artigo que expõe os benefícios do uso da opção NOEXPAND. Ele também recomenda que essa opção seja usada com a edição corporativa para garantir que a garantia de exclusividade fornecida pelos índices de exibição seja usada pelo otimizador.

A análise acima foi feita com a edição expressa do SQL Sever 2014. Também tentei com a edição do desenvolvedor do SQL Server 2016. A opção NOEXPAND não parece ser necessária com a edição de desenvolvimento para obter ganhos de desempenho, mas ainda é recomendada .

Há menos de 5 meses, a Microsoft disponibilizou as edições do desenvolvedor . A licença restringe o uso apenas ao desenvolvimento, o que significa que o banco de dados não pode ser usado em um ambiente de produção. Portanto, se você estiver procurando testar tabelas, criptografia, R etc. otimizadas para memória, não terá mais a desculpa da ausência de licença. Eu o instalei com sucesso no meu computador há alguns dias, ao lado do SQL Server 2014 Express, sem problemas.

Bob Bryan
fonte