Índices filtrados são incrivelmente poderosos, mas ainda vejo alguma confusão sobre eles – particularmente sobre as colunas que são usadas nos filtros e o que acontece quando você deseja apertar os filtros.
Uma pergunta recente no dba.stackexchange pediu ajuda sobre por que as colunas usadas no filtro de um índice filtrado devem ser incluídas nas colunas 'incluídas' do índice. Excelente pergunta - exceto que eu senti que começou com uma premissa ruim, porque essas colunas não deveriam ser incluídas no índice . Sim, eles ajudam, mas não da maneira que a pergunta parecia sugerir.
Para evitar que você veja a pergunta em si, aqui está um resumo rápido:
Para satisfazer esta pergunta…
SELECT Id, DisplayName FROM Users WHERE Reputation > 400000;
…o seguinte índice filtrado é muito bom:
CREATE UNIQUE NONCLUSTERED INDEX Users_400k_Club ON dbo.Users ( DisplayName, Id ) INCLUDE ( Reputation ) WHERE Reputation > 400000;
Mas apesar de ter esse índice em vigor, o Otimizador de consulta recomenda o índice a seguir se o valor filtrado for reduzido para, digamos, 450.000.
CREATE NONCLUSTERED INDEX IndexThatWasMissing ON dbo.Users ( Reputation ) INCLUDE ( DisplayName, Id );
Estou parafraseando um pouco a pergunta aqui, que começa referindo-se a essa situação e depois constrói um exemplo diferente, mas a ideia é a mesma. Eu só não queria tornar as coisas mais complicadas envolvendo uma tabela separada.
O ponto é – o índice sugerido pelo QO é o índice original, mas invertido. O índice original tinha Reputação na lista INCLUDE e DisplayName e Id como colunas-chave, enquanto o novo índice recomendado é o oposto com Reputação como a coluna-chave e DisplayName &ID no INCLUDE. Vamos ver o porquê.
A pergunta se refere a uma postagem de Erik Darling, onde ele explica que ajustou a consulta '450.000' acima colocando Reputação na coluna INCLUDE. Erik mostra que sem Reputação na lista INCLUDE, uma consulta que filtra para um valor mais alto de Reputação precisa fazer Pesquisas (ruins!), ou talvez até desistir inteiramente do índice filtrado (potencialmente ainda pior). Ele conclui que ter a coluna Reputação na lista INCLUDE permite que o SQL tenha estatísticas, para que possa fazer melhores escolhas, e mostra que com Reputação no INCLUDE uma variedade de consultas que filtram em valores de Reputação mais altos, todas varrem seu índice filtrado.
Em uma resposta à pergunta dba.stackexchange, Brent Ozar aponta que as melhorias de Erik não são particularmente boas porque causam Scans. Voltarei a esse, porque é um ponto interessante em si, e um pouco incorreto.
Primeiro vamos pensar um pouco sobre índices em geral.
Um índice fornece uma estrutura ordenada para um conjunto de dados. (Eu poderia ser pedante e apontar que ler os dados em um índice do início ao fim pode pular de uma página para outra de uma maneira aparentemente aleatória, mas ainda assim você está lendo as páginas, seguindo os ponteiros de uma página para outra a seguir, você pode ter certeza de que os dados estão ordenados. Dentro de cada página, você pode até pular para ler os dados em ordem, mas há uma lista mostrando quais partes (slots) da página devem ser lidas em qual ordem. não adianta meu pedantismo, exceto responder àqueles igualmente pedantes que comentarão se eu não o fizer.)
E essa ordem é de acordo com as colunas principais – essa é a parte mais fácil que todo mundo consegue. É útil não apenas para evitar reordenar os dados posteriormente, mas também para localizar rapidamente qualquer linha ou intervalo de linhas específico por essas colunas.
Os níveis folha do índice contêm os valores em qualquer coluna na lista INCLUDE ou, no caso de um Índice Agrupado, os valores em todas as colunas da tabela (exceto colunas computadas não persistentes). Os outros níveis no índice contêm apenas as colunas de chave e (se o índice não for exclusivo) o endereço exclusivo da linha – que são as chaves do índice clusterizado (com o unificador da linha se o índice clusterizado também não for exclusivo ) ou o valor RowID para um heap, o suficiente para permitir acesso fácil a todos os outros valores de coluna da linha. Os níveis de folha também incluem todas as informações de ‘endereço’.
Mas essa não é a parte interessante para este post. A parte interessante para este post é o que quero dizer com "para um conjunto de dados". Lembre-se que eu disse "Um índice fornece uma estrutura ordenada para um conjunto de dados ".
Em um índice clusterizado, esse conjunto de dados é a tabela inteira, mas pode ser outra coisa. Você provavelmente já pode imaginar como a maioria dos índices não clusterizados não envolve todas as colunas da tabela. Essa é uma das coisas que tornam os índices não clusterizados tão úteis, porque normalmente são muito menores do que a tabela subjacente.
No caso de uma visualização indexada, nosso conjunto de dados pode ser o resultado de uma consulta inteira, incluindo junções em várias tabelas! Isso fica para outro post.
Mas em um índice filtrado, não é apenas uma cópia de um subconjunto de colunas, mas também um subconjunto de linhas. Portanto, no exemplo aqui, o índice é apenas entre os usuários com mais de 400 mil reputação.
CREATE UNIQUE NONCLUSTERED INDEX Users_400k_Club_NoInclude ON dbo.Users ( DisplayName, Id ) WHERE Reputation > 400000;
Esse índice pega os usuários que têm mais de 400 mil reputação e os ordena por DisplayName e Id. Pode ser exclusivo porque (supostamente) a coluna Id já é exclusiva. Se você tentar algo semelhante em sua própria mesa, talvez seja necessário ter cuidado com isso.
Mas neste ponto, o índice não se importa com a Reputação de cada usuário – ele apenas se importa se a Reputação é alta o suficiente para estar no índice ou não. Se a reputação de um usuário for atualizada e ultrapassar o limite, o DisplayName e o Id do usuário serão inseridos no índice. Se cair abaixo, será excluído do índice. É como ter uma mesa separada para os grandes apostadores, exceto que colocamos as pessoas nessa mesa aumentando seu valor de Reputação acima do limite de 400k na tabela subjacente. Ele pode fazer isso sem ter que armazenar o próprio valor de Reputação.
Então, agora, se quisermos encontrar pessoas que tenham um limite acima de 450k, esse índice está faltando algumas informações.
Claro, podemos dizer com segurança que todos que encontrarmos estão nesse índice – mas o índice não contém informações suficientes para filtrar ainda mais a Reputação. Se eu te dissesse que tenho uma lista alfabética de filmes vencedores do Oscar de Melhor Filme da década de 1990 (Beleza Americana, Coração Valente, Danças Com Lobos, Paciente Inglês, Forrest Gump, A Lista de Schindler, Shakespeare Apaixonado, Silêncio dos Inocentes, Titanic, Imperdoável) , posso garantir que os vencedores de 1994-1996 seriam um subconjunto deles, mas não posso responder à pergunta sem primeiro obter mais informações.
Obviamente, meu índice filtrado seria mais útil se eu incluísse o ano, e potencialmente ainda mais se o ano fosse uma coluna-chave, pois minha nova consulta deseja encontrar os de 1994-1996. Mas provavelmente projetei esse índice em torno de uma consulta para listar todos os filmes da década de 1990 em ordem alfabética. Essa consulta não se importa com o ano real, apenas se é na década de 1990 ou não, e eu nem preciso retornar o ano – apenas o título – para que eu possa escanear meu índice filtrado para obter os resultados. Para essa consulta, nem preciso reordenar os resultados ou encontrar o ponto de partida – meu índice é realmente perfeito.
Um exemplo mais prático de não se importar com o valor da coluna no filtro é no status, como:
WHERE IsActive = 1
Frequentemente vejo código que move dados de uma tabela para outra quando as linhas param de ser 'ativas'. As pessoas não querem que as linhas antigas atravessem suas tabelas e reconhecem que seus dados 'quentes' são apenas um pequeno subconjunto de todos os seus dados. Assim, eles movem seus dados de resfriamento para uma tabela Archive, mantendo sua tabela ativa pequena.
Um índice filtrado pode fazer isso por você. Por trás das cenas. Assim que você atualizar a linha e alterar a coluna IsActive para algo diferente de 1. Se você se preocupa apenas em ter dados ativos na maioria de seus índices, os índices filtrados são ideais. Ele até trará linhas de volta aos índices se o valor IsActive mudar de volta para 1.
Mas você não precisa colocar IsActive na lista INCLUDE para conseguir isso. Por que você deseja armazenar o valor – você já sabe qual é o valor – é 1! A menos que você esteja pedindo para devolver o valor, você não deve precisar dele. E por que você devolveria o valor quando já sabe que a resposta é 1, certo?! Exceto que frustrantemente, as estatísticas às quais Erik se refere em seu post tirarão vantagem de estar na lista INCLUDE. Você não precisa dele para a consulta, mas deve incluí-lo nas estatísticas.
Vamos pensar no que o Otimizador de Consultas precisa fazer para descobrir a utilidade de um índice.
Antes de poder fazer muita coisa, ele precisa considerar se o índice é um candidato. Não adianta usar um índice se ele não tiver todas as linhas que podem ser necessárias – a menos que tenhamos uma maneira eficaz de obter o resto. Se eu quero filmes de 1985-1995, então meu índice de filmes dos anos 1990 é bastante inútil. Mas para 1994-1996, talvez não seja ruim.
Neste ponto, assim como qualquer consideração de índice, preciso pensar se isso ajudará o suficiente para encontrar os dados e colocá-los em uma ordem que ajude a executar o restante da consulta (possivelmente para um Merge Join, Stream Aggregate, um ORDER BY, ou vários outros motivos). Se meu filtro de consulta corresponder exatamente ao filtro de índice, não preciso filtrar mais – basta usar o índice. Isso parece ótimo, mas se não corresponder exatamente, se meu filtro de consulta for mais apertado que o filtro de índice (como meu exemplo de 1994-1996 ou o 450.000 de Erik), precisarei ter esses valores de ano ou valores de reputação para verificar - espero obtê-los do INCLUDEd no nível da folha ou em algum lugar nas minhas colunas-chave. Se eles não estiverem no índice, vou ter que fazer um Lookup para cada linha do meu índice filtrado (e, idealmente, ter uma ideia de quantas vezes meu Lookup será chamado, que são as estatísticas que Erik quer a coluna incluída para).
Idealmente, qualquer índice que pretendo usar é ordenado corretamente (por meio das chaves), INCLUI todas as colunas que preciso retornar e é pré-filtrado apenas para as linhas de que preciso. Esse seria o índice perfeito, e meu plano de execução será um Scan.
Isso mesmo, um SCAN. Não uma busca, mas uma varredura. Ele começará na primeira página do meu índice e continuará me dando linhas até que eu tenha tantas quantas eu preciso, ou até que não haja mais linhas para retornar. Sem pular nenhuma, sem classificá-las – apenas me dando as linhas em ordem.
Um Seek sugeriria que eu não preciso de todo o índice, o que significa que estou desperdiçando recursos na manutenção dessa parte do índice e, para consultá-lo, tenho que encontrar o ponto de partida e continuar verificando as linhas para ver se chegar ao fim ou não. Se meu Scan tiver um Predicate, com certeza, estou tendo que examinar (e testar) mais dados do que preciso, mas se meus filtros de índice forem perfeitos, o Query Optimizer deve reconhecer isso e não precisar realizar essas verificações .
Considerações finais
INCLUDEs não são críticos para índices filtrados. Eles são úteis para fornecer acesso fácil a colunas que podem ser úteis para sua consulta. A mistura. Mas nesse ponto você deve estar perguntando se o filtro do seu índice é o correto, o que mais você deve ter em sua lista INCLUDE e até mesmo quais devem ser as colunas-chave. As consultas de Erik não estavam funcionando bem porque ele precisava de informações que não estavam no índice, embora ele tivesse mencionado a coluna no filtro. Ele também encontrou um bom uso para as estatísticas, e eu ainda o encorajo a incluir as colunas de filtro por esse motivo. Mas colocá-los em um INCLUDE não permite que eles de repente comecem a fazer um Seek, porque não é assim que qualquer índice funciona, seja filtrado ou não.
Quero que você, leitor, entenda muito bem os índices filtrados. Eles são incrivelmente úteis e, quando você começa a imaginá-los como tabelas por si só, podem se tornar parte do design geral do banco de dados. Eles também são um motivo para sempre usar as configurações ANSI_NULLs e QUOTED_IDENTIFIER, porque você receberá erros do índice filtrado, a menos que essas configurações estejam ATIVADAS, mas esperamos que você já tenha certeza de que elas estão sempre ativadas de qualquer maneira.
Ah, e esses filmes eram Forrest Gump, Coração Valente e O Paciente Inglês.
@rob_farley