Ce que le Query Store enregistre

Ce que le Query Store garde d’une requête et ce qu’il laisse de côté : instructions absentes, exécutions interrompues, pertes après un arrêt brutal.

Article coécrit avec Claude, qui a mené les tests en laboratoire et une partie des recherches.

Le Query Store est un enregistreur de requêtes intégré à chaque base, depuis SQL Server 2016. Il conserve, dans la base elle-même, le texte des requêtes, leurs plans d’exécution et leurs statistiques d’exécution par intervalle de temps, et, depuis SQL Server 2017, leurs attentes. Il sert à retrouver une requête qui a ralenti, à comparer ses plans, et à forcer celui qui convient. On le lit par les vues sys.query_store_* de la base, ou par les rapports de SSMS.

Il ne garde pas tout. Ce qu’il laisse de côté dépend du mode de capture, de l’instruction, de la base où elle s’exécute et de la façon dont l’exécution se termine. Cet article en fait l’inventaire. Les mesures viennent de conteneurs Linux SQL Server 2025 CU9 avec sa mise à jour de sécurité (17.0.5015.4), en mode ALL sauf mention contraire. Il reprend aussi deux articles voisins : les modes de capture et l’analyse du Query Store par un agent.

Le contenu du Query Store

VueContenu
sys.query_store_query_textle texte de l’instruction, tel qu’il a été compilé
sys.query_store_queryla requête : la référence à son texte (query_text_id), ses options de contexte (SET), l’objet qui la contient (object_id), ses statistiques de compilation
sys.query_store_planles plans de la requête, 200 au plus par défaut (MAX_PLANS_PER_QUERY), et l’état de leur forçage
sys.query_store_runtime_statspar plan, par intervalle (60 minutes par défaut) et par type de fin d’exécution : nombre d’exécutions, durée, CPU, lectures, écritures, lignes, mémoire
sys.query_store_wait_statsles attentes, regroupées par catégorie et non par type d’attente

Le texte enregistré est celui de l’instruction, pas celui du lot. Quand la paramétrisation simple s’applique, c’est le texte réécrit par le moteur : les littéraux deviennent des paramètres, les noms sont mis entre crochets, et les commentaires disparaissent. Dans ma base de test, SELECT /*marqueur*/ COUNT(*) FROM dbo.T WHERE id < 7 est enregistré sous (@1 tinyint)SELECT COUNT(*) FROM [dbo].[T] WHERE [id]<@1. Chercher le marqueur ne le trouve pas.

Le plan enregistré est un plan estimé. Il contient les valeurs des paramètres à la compilation (ParameterCompiledValue), pas celles des exécutions suivantes. Une procédure compilée avec @id = 5 puis exécutée avec 7 et 9 n’a que la valeur 5 dans son plan. Pour une requête paramétrée par la paramétrisation simple, la valeur compilée est le littéral de la première exécution.

Aucune colonne des vues sys.query_store_* ne contient un nom de login, d’utilisateur, de poste ou d’application, ni un numéro de session (sys.query_store_replicas, depuis SQL Server 2022, ne nomme que les réplicas d’un groupe de disponibilité). Le Query Store dit ce qui a été exécuté et combien de fois, pas par qui. Pour le savoir, il faut un audit (SQL Server Audit ou une session d’évènements étendus) en place avant les faits.

Le mode de capture

Le mode ALL capture toutes les requêtes dès leur première exécution, sauf celles des procédures compilées en natif, dont il faut activer la collecte par sys.sp_xtp_control_query_exec_stats. Le mode AUTO, par défaut depuis SQL Server 2019, n’en enregistre une qu’au moment où elle atteint, sur une période d’évaluation, 30 exécutions, une seconde de CPU de compilation ou 100 ms de CPU d’exécution. Les exécutions qui précèdent ce moment ne sont pas comptées. Les requêtes des procédures stockées sont capturées dès leur première exécution ; les curseurs l’étaient jusqu’en SQL Server 2019, ils sont soumis aux seuils depuis SQL Server 2022. Le mode NONE n’enregistre plus aucune requête nouvelle. Le détail et les mesures sont dans l’article sur les modes de capture.

Quel que soit le mode, un Query Store en READ_ONLY n’enregistre plus rien. Il y passe de lui-même quand il dépasse MAX_STORAGE_SIZE_MB, et revient en lecture-écriture si le nettoyage par la taille libère assez de place. Un Query Store désactivé (OFF) garde son contenu, figé à la date de la désactivation.

Les instructions absentes

J’ai lancé une instruction de chaque sorte dans une base neuve en mode ALL, chacune avec un commentaire marqueur dans son texte et des variables plutôt que des littéraux, pour que la paramétrisation simple ne retire pas le marqueur :

InstructionCapturée
SELECT, INSERT, UPDATE, DELETE, MERGE, SELECT ... INTOoui
sous-requête d’un SET @x = (SELECT ...) ou d’un IF EXISTS (SELECT ...)oui
requête d’une procédure stockée ou d’un déclencheur, SQL dynamique par EXEC()oui
requête sur une variable de table ou une table temporaireoui
CREATE INDEX, ALTER INDEX ... REBUILD, UPDATE STATISTICSsous leur texte, comme requêtes internes
TRUNCATE TABLEnon
CREATE TABLE, ALTER TABLE, DROP TABLEnon
DBCC CHECKTABLEnon

Pour la création ou la reconstruction d’un index et la mise à jour des statistiques, le Query Store garde le texte de la commande, mais comme requête interne (is_internal_query = 1), avec le plan de la requête que le moteur exécute pour elle : un INSERT qui remplit l’index, un SELECT qui calcule les statistiques. La documentation le dit : le Query Store ne collecte pas les plans des instructions DDL, mais ceux des instructions DML qu’elles exécutent. TRUNCATE TABLE, CREATE TABLE, ALTER TABLE, DROP TABLE et DBCC CHECKTABLE ne laissent aucune trace, même en ALL : une table vidée par TRUNCATE ne se retrouve pas dans le Query Store.

La base de contexte

Une requête est enregistrée dans le Query Store de la base dans laquelle elle s’exécute, pas dans celui de la base qu’elle lit. Lancée depuis la base Ventes, la requête suivante apparaît dans le Query Store de Ventes, et pas dans celui de PachaDataFormation :

SELECT * FROM PachaDataFormation.Contact.Adresse WHERE AdresseId = 4812;

Le Query Store ne peut être activé ni sur master ni sur tempdb. Une requête ad hoc lancée depuis l’une de ces deux bases n’est donc enregistrée nulle part, quelle que soit la base qu’elle lit. Or tout login peut s’y connecter. Une procédure d’une base utilisateur appelée depuis master, ou un sp_executesql préfixé par le nom de la base, s’exécute dans cette base et y est enregistré. Une requête lancée depuis une base utilisateur et qui lit tempdb est, elle, enregistrée dans la base utilisateur.

Les exécutions interrompues

sys.query_store_runtime_stats distingue trois fins d’exécution par sa colonne execution_type_desc : Regular (terminée normalement), Aborted (annulée par le client) et Exception (interrompue par une erreur). J’ai interrompu des requêtes de quatre façons, dans une base neuve en ALL, sur une requête de 3 à 5 secondes pour les trois dernières, qui donnent le même résultat sur SQL Server 2017, 2019, 2022 et 2025 :

Fin de l’exécutionCe que le Query Store enregistre
erreur (division par zéro)une exécution Exception
délai d’attente du client (sqlcmd -t 3)une exécution Aborted, avec sa durée jusqu’à l’annulation
processus client tué (kill -9)une exécution Aborted, avec sa durée
session tuée par KILL depuis une autre sessionle texte et le plan, aucune statistique d’exécution

La page How Query Store collects data dit que les statistiques d’une requête ne sont pas enregistrées quand la session est terminée ou quand l’application cliente redémarre ou plante. C’est vrai pour la session terminée par KILL. Pour un client tué dont la connexion se ferme, je mesure le contraire : l’exécution est enregistrée comme annulée, ce que la page de sys.query_store_runtime_stats décrit d’ailleurs comme « Client initiated aborted execution ». J’ai proposé une correction de la page.

Pour lire ces exécutions, filtrez sur le type plutôt que de tout additionner :

USE PachaDataFormation;

SELECT q.query_id, rs.execution_type_desc,
       SUM(rs.count_executions) AS executions,
       MAX(rs.max_duration) / 1000 AS duree_max_ms
FROM sys.query_store_query AS q
JOIN sys.query_store_plan AS p ON p.query_id = q.query_id
JOIN sys.query_store_runtime_stats AS rs ON rs.plan_id = p.plan_id
WHERE rs.execution_type_desc <> N'Regular'
GROUP BY q.query_id, rs.execution_type_desc
ORDER BY executions DESC;

L’arrêt brutal de l’instance

Le texte des requêtes et leurs plans sont écrits sur disque avec une latence minimale, dit la documentation. Les statistiques d’exécution restent en mémoire et sont écrites toutes les DATA_FLUSH_INTERVAL_SECONDS, 900 secondes par défaut. J’ai exécuté une requête 10 fois, puis arrêté l’instance de trois façons avant toute écriture, et relu le Query Store après le redémarrage :

ArrêtTexte de la requêteExécutions enregistrées
SHUTDOWNprésent10
SHUTDOWN WITH NOWAITprésent0
conteneur tué (SIGKILL)présent0

Un arrêt normal écrit les statistiques, sauf avec le trace flag 7745, qui supprime cette écriture. Un arrêt sans attente ou une panne efface tout ce qui n’avait pas été écrit, jusqu’à 15 minutes d’exécutions avec la valeur par défaut. Avant un arrêt que vous savez brutal, ou avant de copier une base pour l’analyser, sp_query_store_flush_db écrit sur disque les statistiques en mémoire :

USE PachaDataFormation;

EXEC sys.sp_query_store_flush_db;

La durée de conservation

Les données sont gardées 30 jours par défaut (STALE_QUERY_THRESHOLD_DAYS). Le nettoyage par la taille, actif par défaut, retire les requêtes les moins coûteuses et les plus anciennes quand le Query Store approche de MAX_STORAGE_SIZE_MB, 1 000 Mo par défaut depuis SQL Server 2019. Une requête rare et peu coûteuse est donc la première à disparaître.

Le Query Store est stocké dans la base : une sauvegarde le conserve. DBCC CLONEDATABASE le copie dans le clone quand il est activé dans la base source, sans les statistiques encore en mémoire. NO_QUERYSTORE et VERIFY_CLONEDB l’excluent du clone.