Database
 sql >> Base de données >  >> RDS >> Database

Index filtrés et colonnes INCLUDEd

Les index filtrés sont incroyablement puissants, mais je constate encore une certaine confusion à leur sujet, en particulier au sujet des colonnes utilisées dans les filtres et de ce qui se passe lorsque vous souhaitez resserrer les filtres.

Une question récente sur dba.stackexchange demandait de l'aide sur la raison pour laquelle les colonnes utilisées dans le filtre d'un index filtré devraient être incluses dans les colonnes "incluses" de l'index. Excellente question - sauf que j'avais l'impression que cela commençait sur une mauvaise prémisse, car ces colonnes ne devraient pas avoir à être incluses dans l'index . Oui, ils aident, mais pas de la manière que la question semblait suggérer.

Pour vous éviter de regarder la question elle-même, voici un bref résumé :

Pour satisfaire cette requête…

SELECT Id, DisplayName 
FROM Users 
WHERE Reputation > 400000;

…l'index filtré suivant est plutôt bon :

CREATE UNIQUE NONCLUSTERED INDEX Users_400k_Club
ON dbo.Users ( DisplayName, Id )
INCLUDE ( Reputation )
WHERE Reputation > 400000;

Mais malgré la mise en place de cet index, l'optimiseur de requête recommande l'index suivant si la valeur filtrée est resserrée à, disons, 450 000.

CREATE NONCLUSTERED INDEX IndexThatWasMissing
ON dbo.Users ( Reputation )
INCLUDE ( DisplayName, Id );

Je paraphrase un peu la question ici, qui commence par faire référence à cette situation et construit ensuite un exemple différent, mais l'idée est la même. Je ne voulais tout simplement pas compliquer les choses en impliquant une table séparée.

Le point est - l'index suggéré par le QO est l'index d'origine mais inversé. L'index d'origine avait Reputation dans la liste INCLUDE, et DisplayName et Id comme colonnes clés, tandis que le nouvel index recommandé est l'inverse avec Reputation comme colonne clé et DisplayName &ID dans INCLUDE. Voyons pourquoi.

La question fait référence à un article d'Erik Darling, dans lequel il explique qu'il a réglé la requête "450 000" ci-dessus en plaçant Reputation dans la colonne INCLUDE. Erik montre que sans réputation dans la liste INCLUDE, une requête qui filtre vers une valeur supérieure de réputation doit faire des recherches (mauvais !), Ou peut-être même abandonner complètement l'index filtré (potentiellement encore pire). Il conclut que le fait d'avoir la colonne Réputation dans la liste INCLUDE permet à SQL d'avoir des statistiques, de sorte qu'il peut faire de meilleurs choix, et montre qu'avec Réputation dans l'INCLUDE, une variété de requêtes qui filtrent toutes sur des valeurs de réputation plus élevées analysent toutes son index filtré.

Dans une réponse à la question dba.stackexchange, Brent Ozar souligne que les améliorations d'Erik ne sont pas particulièrement importantes car elles provoquent des scans. Je reviendrai sur celui-là, car c'est un point intéressant en soi, et quelque peu incorrect.

Réfléchissons d'abord un peu aux index en général.

Un index fournit une structure ordonnée à un ensemble de données. (Je pourrais être pédant et souligner que la lecture des données d'un index du début à la fin peut vous faire sauter d'une page à l'autre d'une manière apparemment aléatoire, mais toujours pendant que vous lisez des pages, en suivant les pointeurs d'une page à la suivante, vous pouvez être sûr que les données sont ordonnées. Dans chaque page, vous pouvez même sauter pour lire les données dans l'ordre, mais il y a une liste vous indiquant quelles parties (emplacements) de la page doivent être lues dans quel ordre. Il y a vraiment n'a aucun sens dans mon pédantisme, sauf pour répondre à ceux tout aussi pédants qui commenteront si je ne le fais pas.)

Et cet ordre est selon les colonnes clés - c'est la partie facile que tout le monde obtient. C'est utile non seulement pour pouvoir éviter de réorganiser les données plus tard, mais aussi pour pouvoir localiser rapidement une ligne ou une plage de lignes particulière par ces colonnes.

Les niveaux feuille de l'index contiennent les valeurs de toutes les colonnes de la liste INCLUDE ou, dans le cas d'un index clusterisé, les valeurs de toutes les colonnes de la table (à l'exception des colonnes calculées non persistantes). Les autres niveaux de l'index contiennent uniquement les colonnes clés et (si l'index n'est pas unique) l'adresse unique de la ligne - qui est soit les clés de l'index clusterisé (avec l'unificateur de ligne si l'index clusterisé n'est pas unique non plus ) ou la valeur RowID pour un tas, suffisamment pour permettre un accès facile à toutes les autres valeurs de colonne pour la ligne. Les niveaux feuille incluent également toutes les informations "d'adresse".

Mais ce n'est pas la partie intéressante de ce post. La partie intéressante de cet article est ce que j'entends par "à un ensemble de données". Rappelez-vous que j'ai dit "Un index fournit une structure ordonnée à un ensemble de données ".

Dans un index clusterisé, cet ensemble de données correspond à la table entière, mais il peut s'agir d'autre chose. Vous pouvez probablement déjà imaginer que la plupart des index non clusterisés n'impliquent pas toutes les colonnes de la table. C'est l'une des choses qui rendent les index non clusterisés si utiles, car ils sont généralement beaucoup plus petits que la table sous-jacente.

Dans le cas d'une vue indexée, notre ensemble de données pourrait être le résultat d'une requête complète, y compris des jointures sur de nombreuses tables ! C'est pour un autre article.

Mais dans un index filtré, ce n'est pas seulement une copie d'un sous-ensemble de colonnes, mais aussi un sous-ensemble de lignes. Ainsi, dans l'exemple ici, l'index ne concerne que les utilisateurs ayant plus de 400 000 réputations.

CREATE UNIQUE NONCLUSTERED INDEX Users_400k_Club_NoInclude
ON dbo.Users ( DisplayName, Id )
WHERE Reputation > 400000;

Cet index prend les utilisateurs qui ont plus de 400 000 réputations et les classe par DisplayName et Id. Il peut être unique car (supposément) la colonne Id est déjà unique. Si vous essayez quelque chose de similaire sur votre propre table, vous devrez peut-être faire attention à cela.

Mais à ce stade, l'index ne se soucie pas de la réputation de chaque utilisateur - il se soucie simplement de savoir si la réputation est suffisamment élevée pour figurer dans l'index ou non. Si la réputation d'un utilisateur est mise à jour et qu'elle dépasse le seuil, le nom d'affichage et l'identifiant de cet utilisateur seront insérés dans l'index. S'il tombe en dessous, il sera supprimé de l'index. C'est comme avoir une table séparée pour les gros joueurs, sauf que nous faisons entrer les gens dans cette table en augmentant leur valeur de réputation au-dessus du seuil de 400 000 dans la table sous-jacente. Il peut le faire sans avoir à stocker la valeur de réputation elle-même.

Alors maintenant, si nous voulons trouver des personnes qui ont un seuil supérieur à 450 000, il manque certaines informations à cet index.

Bien sûr, nous pouvons dire avec confiance que tout le monde que nous trouverons est dans cet index - mais l'index ne contient pas suffisamment d'informations en soi pour filtrer davantage sur la réputation. Si je vous disais que j'avais une liste alphabétique des films primés aux Oscars du meilleur film des années 1990 (American Beauty, Braveheart, Dances With Wolves, English Patient, Forrest Gump, Schindler's List, Shakespeare in Love, Silence of the Lambs, Titanic, Unforgiven) , alors je peux vous assurer que les gagnants pour 1994-1996 seraient un sous-ensemble de ceux-ci, mais je ne peux pas répondre à la question sans obtenir d'abord plus d'informations.

Évidemment, mon index filtré serait plus utile si j'avais inclus l'année, et potentiellement encore plus si l'année était une colonne clé, puisque ma nouvelle requête veut trouver celles de 1994-1996. Mais j'ai probablement conçu cet index autour d'une requête pour lister tous les films des années 1990 par ordre alphabétique. Cette requête ne se soucie pas de l'année réelle, seulement si c'est dans les années 1990 ou non, et je n'ai même pas besoin de retourner l'année - juste le titre - donc je peux scanner mon index filtré pour obtenir les résultats. Pour cette requête, je n'ai même pas besoin de réorganiser les résultats ou de trouver le point de départ - mon index est vraiment parfait.

Un exemple plus pratique de ne pas se soucier de la valeur de la colonne dans le filtre est le statut, tel que :

WHERE IsActive = 1

Je vois fréquemment du code qui déplace des données d'une table à une autre lorsque les lignes cessent d'être "actives". Les gens ne veulent pas que d'anciennes lignes encombrent leur table, et ils reconnaissent que leurs données « chaudes » ne sont qu'un petit sous-ensemble de toutes leurs données. Ils déplacent donc leurs données de refroidissement dans une table Archive, en gardant leur table Active petite.

Un index filtré peut le faire pour vous. Dans les coulisses. Dès que vous mettez à jour la ligne et remplacez cette colonne IsActive par autre chose que 1. Si vous vous souciez uniquement d'avoir des données actives dans la plupart de vos index, les index filtrés sont idéaux. Il ramènera même des lignes dans les index si la valeur IsActive revient à 1.

Mais vous n'avez pas besoin de mettre IsActive dans la liste INCLUDE pour y parvenir. Pourquoi voudriez-vous stocker la valeur - vous savez déjà quelle est la valeur - c'est 1 ! À moins que vous ne demandiez à renvoyer la valeur, vous ne devriez pas en avoir besoin. Et pourquoi renverriez-vous la valeur alors que vous savez déjà que la réponse est 1, n'est-ce pas ? ! Sauf que frustrant, les statistiques auxquelles Erik fait référence dans son post profiteront d'être dans la liste INCLUDE. Vous n'en avez pas besoin pour la requête, mais vous devez l'inclure pour les statistiques.

Réfléchissons à ce que l'optimiseur de requête doit faire pour déterminer l'utilité d'un index.

Avant de pouvoir faire quoi que ce soit, il doit se demander si l'indice est un candidat. Il ne sert à rien d'utiliser un index s'il ne contient pas toutes les lignes qui pourraient être nécessaires - à moins que nous ayons un moyen efficace d'obtenir le reste. Si je veux des films de 1985 à 1995, alors mon index des films des années 1990 est assez inutile. Mais pour 1994-1996, c'est peut-être pas mal.

À ce stade, comme pour toute considération d'index, je dois me demander si cela aidera suffisamment pour trouver les données et les mettre dans un ordre qui aidera à exécuter le reste de la requête (éventuellement pour une jointure de fusion, un agrégat de flux, satisfaisant un ORDER BY, ou diverses autres raisons). Si mon filtre de requête correspond exactement au filtre d'index, je n'ai pas besoin de filtrer davantage - il suffit d'utiliser l'index. Cela sonne bien, mais si cela ne correspond pas exactement, si mon filtre de requête est plus serré que le filtre d'index (comme mon exemple 1994-1996, ou les 450 000 d'Erik), je vais avoir besoin de ces valeurs Année ou Valeurs de réputation pour vérifier - j'espère les obtenir soit à partir de INCLUDEd au niveau feuille ou quelque part dans mes colonnes clés. S'ils ne sont pas dans l'index, je vais devoir faire une recherche pour chaque ligne de mon index filtré (et idéalement, avoir une idée du nombre de fois que ma recherche sera appelée, quelles sont les statistiques qu'Erik veut la colonne incluse pour).

Idéalement, tout index que je prévois d'utiliser est ordonné correctement (via les clés), INCLUDE toutes les colonnes que je dois renvoyer et est pré-filtré uniquement pour les lignes dont j'ai besoin. Ce serait l'index parfait, et mon plan d'exécution sera un Scan.

C'est vrai, un SCAN. Pas une recherche, mais un balayage. Il commencera sur la première page de mon index et continuera à me donner des lignes jusqu'à ce que j'en ai autant que nécessaire, ou jusqu'à ce qu'il n'y ait plus de lignes à retourner. Ne pas en sauter, ne pas les trier - me donner juste les lignes dans l'ordre.

Une recherche suggérerait que je n'ai pas besoin de tout l'index, ce qui signifie que je gaspille des ressources pour maintenir cette partie de l'index, et pour l'interroger, je dois trouver le point de départ et continuer à vérifier les lignes pour voir si j'ai toucher la fin ou pas. Si mon analyse a un prédicat, alors bien sûr, je dois parcourir (et tester) plus de données que nécessaire, mais si mes filtres d'index sont parfaits, alors l'optimiseur de requête devrait le reconnaître et ne pas avoir à effectuer ces vérifications .

Réflexions finales

Les INCLUDE ne sont pas critiques pour les index filtrés. Ils sont utiles pour fournir un accès facile aux colonnes qui pourraient être utiles pour votre requête, et si vous resserrez le contenu de votre index filtré par n'importe quelle colonne, qu'elle soit mentionnée dans le filtre ou non, vous devriez envisager d'avoir cette colonne dans le mélange. Mais à ce stade, vous devriez vous demander si le filtre de votre index est le bon, ce que vous devriez avoir d'autre dans votre liste INCLUDE, et même quelle(s) colonne(s) clé(s) devraient être. Les requêtes d'Erik ne fonctionnaient pas bien car il avait besoin d'informations qui ne figuraient pas dans l'index, même s'il avait mentionné la colonne dans le filtre. Il a également trouvé une bonne utilisation des statistiques, et je vous encourage toujours à inclure les colonnes de filtre pour cette raison. Mais les mettre dans un INCLUDE ne leur permet pas de commencer soudainement à faire un Seek, car ce n'est pas comme ça qu'un index fonctionne, qu'il soit filtré ou non.

Je veux que vous, lecteur, compreniez très bien les index filtrés. Ils sont incroyablement utiles et, lorsque vous commencez à les imaginer comme des tables à part entière, ils peuvent faire partie intégrante de la conception globale de votre base de données. Ils sont également une raison pour toujours utiliser les paramètres ANSI_NULLs et QUOTED_IDENTIFIER, car vous obtiendrez des erreurs de l'index filtré à moins que ces paramètres ne soient activés, mais j'espère que vous vous assurez déjà qu'ils sont toujours activés de toute façon.

Oh, et ces films étaient Forrest Gump, Braveheart et The English Patient.

@rob_farley