SQL Server dispose d'un optimiseur basé sur les coûts qui utilise les connaissances sur les différentes tables impliquées dans une requête pour produire ce qu'il décide être le plan le plus optimal dans le temps dont il dispose lors de la compilation. Cette connaissance inclut tous les index existants et leurs tailles et toutes les statistiques de colonne existantes. Une partie de la recherche du plan de requête optimal consiste à essayer de minimiser le nombre de lectures physiques nécessaires lors de l'exécution du plan.
On m'a demandé à plusieurs reprises pourquoi l'optimiseur ne tient pas compte de ce qui se trouve dans le pool de mémoire tampon SQL Server lors de la compilation d'un plan de requête, car cela pourrait certainement accélérer l'exécution d'une requête. Dans cet article, je vais vous expliquer pourquoi.
Déterminer le contenu du pool de tampons
La première raison pour laquelle l'optimiseur ignore le pool de mémoire tampon est que c'est un problème non trivial de déterminer ce qu'il y a dans le pool de mémoire tampon en raison de la façon dont le pool de mémoire tampon est organisé. Les pages de fichiers de données sont contrôlées dans le pool de tampons par de petites structures de données appelées tampons, qui suivent des éléments tels que (liste non exhaustive) :
- L'identifiant de la page (numéro de fichier :numéro de page dans le fichier)
- La dernière fois que la page a été référencée (utilisée par le rédacteur paresseux pour aider à mettre en œuvre l'algorithme le moins récemment utilisé qui crée de l'espace libre en cas de besoin)
- L'emplacement mémoire de la page de 8 Ko dans le pool de mémoire tampon
- Si la page est sale ou non (une page sale contient des modifications qui n'ont pas encore été réécrites dans un stockage durable)
- L'unité d'allocation à laquelle appartient la page (expliquée ici) et l'ID de l'unité d'allocation peuvent être utilisés pour déterminer à quelle table et à quel index la page appartient
Pour chaque base de données contenant des pages dans le pool de mémoire tampon, il existe une liste de hachage de pages, dans l'ordre des ID de page, qui peut être recherchée rapidement pour déterminer si une page est déjà en mémoire ou si une lecture physique doit être effectuée. Cependant, rien ne permet facilement à SQL Server de déterminer quel pourcentage du niveau feuille de chaque index d'une table est déjà en mémoire. Le code devrait analyser la liste complète des tampons de la base de données, à la recherche de tampons qui mappent les pages pour l'unité d'allocation en question. Et plus il y avait de pages en mémoire pour une base de données, plus l'analyse prendrait du temps. Cela coûterait trop cher de le faire dans le cadre de la compilation de requêtes.
Si cela vous intéresse, j'ai écrit un article il y a quelque temps avec du code T-SQL qui analyse le pool de mémoire tampon et donne des métriques, en utilisant le DMV sys.dm_os_buffer_descriptors .
Pourquoi l'utilisation du contenu du pool de tampons serait dangereuse
Supposons qu'il * existe * un mécanisme très efficace pour déterminer le contenu du pool de mémoire tampon que l'optimiseur peut utiliser pour l'aider à choisir l'index à utiliser dans un plan de requête. L'hypothèse que je vais explorer est que si l'optimiseur sait qu'une quantité suffisante d'un index moins efficace (plus grand) est déjà en mémoire, par rapport à l'index le plus efficace (plus petit) à utiliser, il devrait choisir l'index en mémoire car il réduisez le nombre de lectures physiques requises et la requête s'exécutera plus rapidement.
Le scénario que je vais utiliser est le suivant :une table BigTable a deux index non clusterisés, Index_A et Index_B, tous deux couvrant complètement une requête particulière. La requête nécessite une analyse complète du niveau feuille de l'index pour récupérer les résultats de la requête. La table comporte 1 million de lignes. Index_A a 200 000 pages au niveau feuille et Index_B a 1 million de pages au niveau feuille, donc une analyse complète d'Index_B nécessite le traitement de cinq fois plus de pages.
J'ai créé cet exemple artificiel sur un ordinateur portable exécutant SQL Server 2019 avec 8 cœurs de processeur, 32 Go de mémoire et des disques à semi-conducteurs. Le code est le suivant :
CREATE TABLE BigTable ( c1 BIGINT IDENTITY, c2 AS (c1 * 2), c3 CHAR (1500) DEFAULT 'a', c4 CHAR (5000) DEFAULT 'b');GO INSERT INTO BigTable DEFAULT VALUES;GO 1000000 CREATE NONCLUSTERED INDEX Index_A ON BigTable (c2) INCLUDE (c3);-- 5 enregistrements par page =200 000 pagesGO CREATE NONCLUSTERED INDEX Index_B ON BigTable (c2) INCLUDE (c4);-- 1 enregistrement par page =1 million de pagesGO CHECKPOINT;GOEt puis j'ai chronométré les requêtes artificielles :
DBCC DROPCLEANBUFFERS;GO -- Index_A pas en mémoireSELECT SUM (c2) FROM BigTable WITH (INDEX (Index_A));GO-- Temps CPU =796 ms, temps écoulé =764 ms -- Index_A en mémoireSELECT SUM (c2) FROM BigTable WITH (INDEX (Index_A));GO-- Temps CPU =312 ms, temps écoulé =52 ms DBCC DROPCLEANBUFFERS;GO -- Index_B pas en mémoireSELECT SUM (c2) FROM BigTable WITH (INDEX (Index_B));GO- - Temps CPU =2952 ms, temps écoulé =2761 ms -- Index_B en mémoireSELECT SUM (c2) FROM BigTable WITH (INDEX (Index_B));GO-- Temps CPU =1219 ms, temps écoulé =149 msVous pouvez voir quand aucun index n'est en mémoire, Index_A est facilement l'index le plus efficace à utiliser, avec un temps de requête écoulé de 764 ms contre 2 761 ms avec Index_B, et il en va de même lorsque les deux index sont en mémoire. Cependant, si Index_B est en mémoire et que Index_A ne l'est pas, si la requête utilise Index_B (149 ms), elle s'exécutera plus rapidement que si elle utilise Index_A (764 ms).
Laissons maintenant l'optimiseur baser le choix du plan sur ce qui se trouve dans le pool de mémoire tampon…
Si Index_A n'est généralement pas en mémoire et que Index_B est principalement en mémoire, il serait plus efficace de compiler le plan de requête pour utiliser Index_B, pour une requête exécutée à cet instant. Même si Index_B est plus grand et nécessiterait plus de cycles CPU pour parcourir, les lectures physiques sont beaucoup plus lentes que les cycles CPU supplémentaires, donc un plan de requête plus efficace minimise le nombre de lectures physiques.
Cet argument n'est valable, et un plan de requête "utiliser Index_B" n'est plus efficace qu'un plan de requête "utiliser Index_A", si Index_B reste principalement en mémoire, et Index_A reste principalement non en mémoire. Dès que la majeure partie d'Index_A est en mémoire, le plan de requête « utiliser Index_A » serait plus efficace, et le plan de requête « utiliser Index_B » est le mauvais choix.
Les situations dans lesquelles le plan compilé « utiliser Index_B » est moins efficace que le plan « utiliser Index_A » basé sur les coûts sont (généralisation) :
- Index_A et Index_B sont tous les deux en mémoire :le plan compilé prendra presque trois fois plus de temps
- Aucun des index n'est résident en mémoire :le plan compilé prend 3,5 fois plus de temps
- Index_A est résident en mémoire et Index_B ne l'est pas :toutes les lectures physiques effectuées par le plan sont superflues, ET cela prendra 53 fois plus longtemps
Résumé
Bien que dans notre exercice de réflexion, l'optimiseur puisse utiliser les connaissances du pool de mémoire tampon pour compiler la requête la plus efficace à un instant donné, ce serait une manière dangereuse de piloter la compilation du plan en raison de la volatilité potentielle du contenu du pool de mémoire tampon, rendant l'efficacité future de le plan en cache est très peu fiable.
N'oubliez pas que le travail de l'optimiseur consiste à trouver rapidement un bon plan, pas nécessairement le meilleur plan pour 100 % de toutes les situations. À mon avis, l'optimiseur SQL Server fait ce qu'il faut en ignorant le contenu réel du pool de mémoire tampon SQL Server et s'appuie plutôt sur les différentes règles d'établissement des coûts pour produire un plan de requête susceptible d'être le plus efficace la plupart du temps. .