Historial y regresiones con Query Store
El plan que vemos en una ejecución no explica cómo se comportó la consulta ayer ni qué estrategia utilizó durante otro intervalo. Query Store conserva texto, planes y estadísticas de ejecución dentro de la base para poder relacionar esos cambios en el tiempo.
Qué conserva Query Store
Query Store organiza la evidencia en tres piezas:
- una consulta identificada por
query_id; - uno o varios planes identificados por
plan_id; - estadísticas de ejecución agregadas por intervalos.
No es lo mismo que la caché de planes. La caché puede expulsar entradas y está orientada a reutilizar planes; Query Store persiste historial en la base según su configuración y política de retención.
Habilitarlo es una decisión de la base
ALTER DATABASE BaseDePrueba
SET QUERY_STORE = ON;
ALTER DATABASE BaseDePrueba
SET QUERY_STORE
(
OPERATION_MODE = READ_WRITE,
QUERY_CAPTURE_MODE = ALL,
INTERVAL_LENGTH_MINUTES = 1,
MAX_STORAGE_SIZE_MB = 100
);
Sustituye BaseDePrueba por el nombre de la base destinada a esta prueba.
Esta configuración se utiliza para una prueba pequeña. En una base real deben definirse captura, retención, tamaño máximo, limpieza y supervisión del estado. Cuando Query Store alcanza su límite o encuentra un problema puede dejar de capturar, por lo que desired_state_desc y actual_state_desc deben revisarse por separado.
Generar dos planes en una base de prueba
El siguiente bloque deshabilita temporalmente IX_PedidoLaboratorio_FechaPedido, ejecuta el mismo texto y reconstruye el índice antes de terminar. Debe utilizarse únicamente en una base de prueba donde ese índice y dbo.PedidoLaboratorio se hayan creado para esta comprobación. No debe ejecutarse sobre una base operativa.
DECLARE @Consulta nvarchar(max) = N'
DECLARE @Desde date = ''2025-12-30'';
SELECT COUNT_BIG(*) AS Pedidos
FROM dbo.PedidoLaboratorio AS p
WHERE p.FechaPedido >= @Desde;';
DECLARE @Ejecucion tinyint = 1;
BEGIN TRY
-- Primer grupo: el índice está disponible.
WHILE @Ejecucion <= 5
BEGIN
EXEC sys.sp_executesql @Consulta;
SET @Ejecucion += 1;
END;
ALTER INDEX IX_PedidoLaboratorio_FechaPedido
ON dbo.PedidoLaboratorio DISABLE;
-- Segundo grupo: el mismo texto se compila sin ese índice.
SET @Ejecucion = 1;
WHILE @Ejecucion <= 5
BEGIN
EXEC sys.sp_executesql @Consulta;
SET @Ejecucion += 1;
END;
ALTER INDEX IX_PedidoLaboratorio_FechaPedido
ON dbo.PedidoLaboratorio REBUILD;
-- Tercer grupo: comprueba el entorno restaurado.
SET @Ejecucion = 1;
WHILE @Ejecucion <= 5
BEGIN
EXEC sys.sp_executesql @Consulta;
SET @Ejecucion += 1;
END;
END TRY
BEGIN CATCH
IF EXISTS
(
SELECT 1
FROM sys.indexes
WHERE object_id = OBJECT_ID(N'dbo.PedidoLaboratorio')
AND name = N'IX_PedidoLaboratorio_FechaPedido'
AND is_disabled = 1
)
BEGIN
ALTER INDEX IX_PedidoLaboratorio_FechaPedido
ON dbo.PedidoLaboratorio REBUILD;
END;
THROW;
END CATCH;
EXEC sys.sp_query_store_flush_db;
El TRY/CATCH protege la restauración del índice si una ejecución falla. Aun con esa protección, deshabilitar un índice cambia la estructura disponible y puede bloquear trabajo concurrente; por eso este bloque pertenece exclusivamente a una prueba controlada.
La consulta y el plan tienen identificadores distintos
query_id representa la consulta que Query Store agrupó. plan_id identifica una estrategia que quedó asociada con ella. Encontrar dos plan_id no demuestra por sí solo una regresión: primero hay que relacionar cada plan con el intervalo, el número de ejecuciones y las métricas observadas.
La prueba ejecuta la misma consulta con el índice disponible, temporalmente deshabilitado y después reconstruido. La operación queda limitada a la base de prueba y el índice se restaura incluso si ocurre un error.
Los intervalos reúnen varias ejecuciones
La cuadrícula de la captura se obtiene ejecutando esta consulta y abriendo Results:
SELECT
actual_state_desc AS EstadoActual,
desired_state_desc AS EstadoDeseado,
current_storage_size_mb AS EspacioActualMB,
max_storage_size_mb AS EspacioMaximoMB,
interval_length_minutes AS IntervaloMinutos
FROM sys.database_query_store_options;
SELECT
q.query_id AS QueryId,
p.plan_id AS PlanId,
rsi.start_time AS InicioIntervalo,
rsi.end_time AS FinIntervalo,
SUM(rs.count_executions) AS Ejecuciones,
CONVERT
(
decimal(12, 2),
SUM(rs.avg_duration * rs.count_executions)
/ NULLIF(SUM(rs.count_executions), 0) / 1000.0
) AS DuracionPromedioMs,
CONVERT
(
decimal(12, 2),
SUM(rs.avg_cpu_time * rs.count_executions)
/ NULLIF(SUM(rs.count_executions), 0) / 1000.0
) AS CpuPromedioMs,
CONVERT
(
decimal(12, 2),
SUM(rs.avg_logical_io_reads * rs.count_executions)
/ NULLIF(SUM(rs.count_executions), 0)
) AS LecturasLogicasPromedio
FROM sys.query_store_query_text AS qt
INNER JOIN sys.query_store_query AS q
ON q.query_text_id = qt.query_text_id
INNER JOIN sys.query_store_plan AS p
ON p.query_id = q.query_id
INNER JOIN sys.query_store_runtime_stats AS rs
ON rs.plan_id = p.plan_id
INNER JOIN sys.query_store_runtime_stats_interval AS rsi
ON rsi.runtime_stats_interval_id = rs.runtime_stats_interval_id
WHERE REPLACE(REPLACE(qt.query_sql_text, CHAR(13), N''), CHAR(10), N' ')
= N'(@Desde date)SELECT COUNT_BIG(*) AS Pedidos FROM dbo.PedidoLaboratorio AS p WHERE p.FechaPedido >= @Desde'
AND q.query_id IN
(
SELECT q2.query_id
FROM sys.query_store_query AS q2
INNER JOIN sys.query_store_plan AS p2
ON p2.query_id = q2.query_id
GROUP BY q2.query_id
HAVING COUNT(DISTINCT p2.plan_id) > 1
)
GROUP BY
q.query_id,
p.plan_id,
rsi.start_time,
rsi.end_time
ORDER BY
q.query_id,
p.plan_id,
rsi.start_time;
Ejecuta el bloque con F5 y abre Results. La primera cuadrícula muestra el estado de Query Store y la segunda reúne los planes por intervalo. Los valores de query_id, plan_id, tiempo e intervalos pertenecen a cada base y pueden diferir de la captura. Lo que buscamos es que una misma consulta conserve más de un plan y que cada fila indique cuántas ejecuciones y qué métricas contribuyeron al intervalo.
- 1Estado de captura
READ_WRITEconfirma que el estado actual coincide con el deseado. El límite configurado es 100 MB y el intervalo de agregación es de un minuto. - 2Dos planes
El
query_id4 conserva losplan_id4 y 5 en los mismos intervalos de la prueba. - 3Trabajo observado
El plan 4 promedia 4 lecturas lógicas; el plan 5 promedia 521. La duración promedio también aumenta, pero debe leerse junto con el número de ejecuciones.
Cada fila resume varias ejecuciones. Un promedio puede ocultar valores extremos; count_executions indica cuántas observaciones contribuyeron. Para un problema intermitente pueden ser necesarios intervalos más pequeños, otra métrica o evidencia adicional.
Cuándo hablar de regresión
Una regresión es un deterioro relativo frente a una referencia comparable. Requiere una condición anterior, una condición posterior y una métrica cuyo cambio tenga significado para la carga.
En esta prueba, el plan 5 realiza 521 lecturas frente a 4 del plan 4 y muestra mayor duración promedio. La diferencia es deliberada y reproducible, pero no autoriza a declarar que todo plan nuevo sea una regresión. También deben compararse parámetros, volumen, número de ejecuciones, concurrencia y resultado.
Recorrer el reporte en SSMS
En Object Explorer, expande la base utilizada para la prueba y después Query Store → Tracked Queries. Escribe en Tracking query el query_id obtenido con la consulta anterior y ejecuta el reporte. Esa pantalla muestra los planes y sus métricas; no es la misma cuadrícula T-SQL de la captura, sino otra vista sobre el mismo historial.
Los reportes de consultas regresadas, consumo de recursos y alta variación ofrecen otros puntos de entrada. Sus nombres y disposición pueden cambiar entre versiones de SSMS.
Las acciones Force Plan y Unforce Plan cambian el comportamiento de la base. No forman parte de esta comprobación: forzar un plan requiere validar compatibilidad, parámetros, estabilidad y un procedimiento de reversión.
Consultar el historial con T-SQL
Las vistas sys.query_store_query_text, sys.query_store_query, sys.query_store_plan, sys.query_store_runtime_stats y sys.query_store_runtime_stats_interval permiten unir texto, consulta, plan, intervalo y métricas. La consulta debe filtrar su alcance; leer todo el historial sin límite puede ser costoso y exponer texto sensible.
Qué podemos concluir
Query Store conserva dos planes para la misma consulta y los relaciona con intervalos, ejecuciones y métricas. En la prueba, un plan promedia 4 lecturas y el otro 521. Esa evidencia permite formular y comprobar una regresión; no reemplaza el contexto de la carga ni convierte el plan más rápido en una decisión permanente.
Siguiente comprobación razonable
La siguiente entrada toma un escenario de Analyze Actual Execution Plan y lo convierte en una hipótesis que pueda contrastarse con el plan, sus propiedades y las métricas ya reunidas.
