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.

Estado de Query Store y estadísticas de dos planes por intervalo

  1. 1
    Estado de captura

    READ_WRITE confirma 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.

  2. 2
    Dos planes

    El query_id 4 conserva los plan_id 4 y 5 en los mismos intervalos de la prueba.

  3. 3
    Trabajo 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.

Relacionado con

Índice del recorrido

Planes de ejecución

30 artículos de Planes de ejecución
  1. 01Para qué sirve un plan de ejecución
  2. 02Plan estimado, plan real y estadísticas en vivo
  3. 03Cómo se organiza el árbol de un plan de ejecución
  4. 04Flechas, propiedades y número de ejecuciones
  5. 05Mapa de operadores: qué papel cumple cada nodo
  6. 06Filas estimadas y filas reales
  7. 07Index Seek, Index Scan y Table Scan
  8. 08Key Lookup y trabajo repetido
  9. 09Filter, Compute Scalar y Top
  10. 10Nested Loops, Hash Match y Merge Join
  11. 11Sort, Segment y Sequence Project
  12. 12Stream Aggregate y Hash Match (Aggregate)
  13. 13Spool: materialización y reutilización
  14. 14Parallelism: distribuir, repartir y reunir filas
  15. 15Advertencias del plan que requieren investigación
  16. 16Acciones disponibles en el plan gráfico de SSMS
  17. 17Cómo se organiza el XML Showplan
  18. 18Recorrer Analyze Actual Execution Plan
  19. 19Lecturas con SET STATISTICS IO
  20. 20CPU y duración con SET STATISTICS TIME
  21. 21Construir una línea base repetible
  22. 22Guardar y comparar dos planes
  23. 23Historial y regresiones con Query Store
  24. 24Convertir un escenario de Analyze Actual Execution Plan en una hipótesis
  25. 25Leer una sugerencia Missing Index en el plan
  26. 26Entender el CREATE INDEX generado por SSMS
  27. 27Decidir, crear y comprobar un índice
  28. 28De una consulta lenta a una hipótesis comprobable
  29. 29Evaluar un cambio sin optimizar solo una ejecución
  30. 30Análisis guiado de una consulta empresarial reproducible