Find queries with several different plans and performances

The same query alternates between fast and slow responses, or Query Store window comparison showed degradation. We want to compare the plans that Query Store kept for that query within a common window.

Query Store preserves texts, plans, and interval execution statistics within the database. A plan is the compiled strategy that SQL Server chose to execute a query; Comparing multiple plans allows you to relate that choice to the observed metrics, but does not automatically attribute the cause of the change.

What we need to distinguish

Recompilation creates the plan again. A schema change modifies object structure, and an estimate is the number of rows SQL Server expects to process; any of these can influence the selected strategy.

Having multiple plans is not necessarily a mistake. Recompilations, schema changes, different estimates or engine characteristics can produce valid alternatives. The comparison should consider sufficient executions and not just the best case of each plan.

Query

/*
    Antes de ejecutar:
        Revisa los permisos y los límites antes de ejecutar la consulta.
        Confirma el alcance, la base activa y el costo posible de la consulta.
        Captura evidencia en varios momentos antes de atribuir una causa.

    Este script es de observación. No aplica correcciones ni modifica datos.
*/

SET NOCOUNT ON;
GO
-- Consulta
DECLARE @Desde DATETIMEOFFSET = DATEADD(DAY, -7, SYSDATETIMEOFFSET());
DECLARE @MinimoEjecuciones BIGINT = 5;

WITH RendimientoPlan AS
(
    SELECT
        p.query_id,
        p.plan_id,
        p.is_forced_plan,
        p.is_parallel_plan,
        SUM(rs.count_executions) AS Ejecuciones,
        SUM(rs.avg_duration * rs.count_executions)
            / NULLIF(SUM(rs.count_executions), 0) / 1000.0
            AS DuracionPromedioMs,
        SUM(rs.avg_cpu_time * rs.count_executions)
            / NULLIF(SUM(rs.count_executions), 0) / 1000.0
            AS CpuPromedioMs,
        SUM(rs.avg_logical_io_reads * rs.count_executions)
            / NULLIF(SUM(rs.count_executions), 0)
            AS LecturasLogicasPromedio,
        MAX(rs.max_duration) / 1000.0 AS DuracionMaximaMs,
        MAX(rs.last_execution_time) AS UltimaEjecucion
    FROM sys.query_store_plan AS p
    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 rs.execution_type = 0
      AND rsi.end_time > @Desde
    GROUP BY
        p.query_id,
        p.plan_id,
        p.is_forced_plan,
        p.is_parallel_plan
),
ConsultasConVarios AS
(
    SELECT rp.query_id
    FROM RendimientoPlan AS rp
    GROUP BY rp.query_id
    HAVING COUNT(*) > 1
       AND SUM(rp.Ejecuciones) >= @MinimoEjecuciones
)
SELECT
    rp.query_id AS IdConsulta,
    rp.plan_id AS IdPlan,
    rp.is_forced_plan AS EsPlanForzado,
    rp.is_parallel_plan AS EsPlanParalelo,
    rp.Ejecuciones,
    CONVERT(DECIMAL(18, 2), rp.DuracionPromedioMs)
        AS DuracionPromedioMs,
    CONVERT(DECIMAL(18, 2), rp.CpuPromedioMs) AS CpuPromedioMs,
    CONVERT(DECIMAL(18, 2), rp.LecturasLogicasPromedio)
        AS LecturasLogicasPromedio,
    CONVERT(DECIMAL(18, 2), rp.DuracionMaximaMs) AS DuracionMaximaMs,
    rp.UltimaEjecucion,
    LEFT(qt.query_sql_text, 2000) AS Consulta
FROM RendimientoPlan AS rp
INNER JOIN ConsultasConVarios AS cv
    ON cv.query_id = rp.query_id
INNER JOIN sys.query_store_query AS q
    ON q.query_id = rp.query_id
INNER JOIN sys.query_store_query_text AS qt
    ON qt.query_text_id = q.query_text_id
ORDER BY
    rp.query_id,
    rp.DuracionPromedioMs DESC,
    rp.Ejecuciones DESC;
GO

Parameters or permissions

Runs the query against a database with Query Store enabled. Adjusts @From and the minimum number of executions to the retention and frequency of the load. VIEW DATABASE STATE, or VIEW DATABASE PERFORMANCE STATE, is required from SQL Server 2022. Metrics are weighted by runs and aggregate rows from the same plan over multiple intervals. The result only includes plans executed during the chosen window.

How to read the result

Compare duration, CPU, readings and number of executions within the same QueryId. A much slower plan with few executions may reflect specific parameters or conditions; a frequent and consistently worse one deserves priority. EsPlanForzado describes the recorded state, but does not demonstrate that the forced has resolved the cause. Retrieves the plan XML from the Query Store or uses its identifier to compare operators and estimates.

What this does establish

It allows you to recognize queries that used more than one plan and compare the aggregate performance of each alternative during a common window.

What this still does not establish

It does not determine which plan should be retained, it does not explain why each was chosen, and it does not guarantee that the historically fastest plan is correct for all parameters.

Next reasonable check

Compare the XML, parameters, execution context and data distribution. Test any fixes in a controlled environment before considering plan forcing, rebuilding, or index changes.

Path index

Troubleshooting and optimization

23 articles in Troubleshooting and optimization
  1. 01View active requests and their SQL text
  2. 02Identify blocking and blocked sessions
  3. 03Build the blocking chain
  4. 04Find long-running open transactions
  5. 05Find idle sessions with an open transaction
  6. 06View waits for active requests
  7. 07Review the main accumulated waits
  8. 08Find queries with the highest total CPU use
  9. 09Find queries with the highest logical reads
  10. 10Find queries with the highest average duration
  11. 11Retrieve a cached execution plan
  12. 12Review missing-index suggestions
  13. 13Review index usage
  14. 14Review index fragmentation with controlled scope
  15. 15Review statistics age and modifications
  16. 16Measure data and log space by database
  17. 17Diagnose why the transaction log cannot be reused
  18. 18Retrieve recent deadlocks from system_health
  19. 19Find regressed queries with Query Store
  20. 20Find sessions and requests consuming tempdb
  21. 21Review pending or excessive memory grants
  22. 22Calculate read and write latency by file
  23. 23Find queries with multiple plans and different performance