Ce que le Query Store enregistre
Categories:
8 minutes à lire
Article coécrit avec Claude, qui a mené les tests en laboratoire et une partie des recherches.
Le Query Store garde le texte, les plans et les statistiques d’exécution des requêtes qu’il capture, mais pas qui les a lancées, ni les valeurs des paramètres au-delà de la compilation. Même en mode ALL, il ne voit ni TRUNCATE TABLE, ni CREATE TABLE, ni ALTER TABLE, ni DBCC CHECKTABLE, ni les requêtes ad hoc lancées depuis master ou tempdb. Une requête en erreur ou annulée par le client est enregistrée, une session tuée par KILL ne laisse que son texte et son plan, et un arrêt brutal de l’instance efface les statistiques qui n’étaient pas encore écrites sur disque.
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
| Vue | Contenu |
|---|---|
sys.query_store_query_text | le texte de l’instruction, tel qu’il a été compilé |
sys.query_store_query | la 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_plan | les 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_stats | par 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_stats | les 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 :
| Instruction | Capturée |
|---|---|
SELECT, INSERT, UPDATE, DELETE, MERGE, SELECT ... INTO | oui |
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 temporaire | oui |
CREATE INDEX, ALTER INDEX ... REBUILD, UPDATE STATISTICS | sous leur texte, comme requêtes internes |
TRUNCATE TABLE | non |
CREATE TABLE, ALTER TABLE, DROP TABLE | non |
DBCC CHECKTABLE | non |
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écution | Ce 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 session | le 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êt | Texte de la requête | Exécutions enregistrées |
|---|---|---|
SHUTDOWN | présent | 10 |
SHUTDOWN WITH NOWAIT | présent | 0 |
conteneur tué (SIGKILL) | présent | 0 |
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.