¿Por qué este CTE recursivo con un parámetro no usa un índice cuando lo hace con un literal?

8

Estoy usando un CTE recursivo en una estructura de árbol para enumerar todos los descendientes de un nodo particular en el árbol. Si escribo un valor de nodo literal en mi WHEREcláusula, SQL Server parece aplicarse en realidad el CTE sólo para ese valor, dando un plan de consulta con bajos recuentos de filas reales, etcétera :

plan de consulta con valor literal

Sin embargo, si paso el valor como parámetro, parece darse cuenta ( poner en cola) el CTE y luego filtrarlo después del hecho :

plan de consulta con valor de parámetro

Podría estar leyendo mal los planes. No he notado un problema de rendimiento, pero me preocupa que la realización del CTE pueda causar problemas con conjuntos de datos más grandes, especialmente en un sistema más ocupado. Además, normalmente compongo este recorrido en sí mismo: atravieso a los antepasados ​​y retrocedo a los descendientes (para asegurarme de reunir todos los nodos relacionados). Debido a cómo son mis datos, cada conjunto de nodos "relacionados" es bastante pequeño, por lo que la realización del CTE no tiene sentido. Y cuando SQL Server parece darse cuenta del CTE, me está dando algunos números bastante grandes en sus recuentos "reales".

¿Hay alguna manera de hacer que la versión parametrizada de la consulta actúe como la versión literal? Quiero poner el CTE en una vista reutilizable.

Consulta con literal:

CREATE PROCEDURE #c AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = 24
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c;

Consulta con parámetro:

CREATE PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t
    WHERE t.ParentId IS NOT NULL
    UNION ALL SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId;
END;
GO
EXEC #c 24;

Código de configuración:

DECLARE @count BIGINT = 100000;
CREATE TABLE #tree (
     Id BIGINT NOT NULL PRIMARY KEY
    ,ParentId BIGINT
);
CREATE INDEX tree_23lk4j23lk4j ON #tree (ParentId);
WITH number AS (SELECT
         CAST(1 AS BIGINT) Value
    UNION ALL SELECT
         n.Value * 2 + 1
    FROM number n
    WHERE n.Value * 2 + 1 <= @count
    UNION ALL SELECT
         n.Value * 2
    FROM number n
    WHERE n.Value * 2 <= @count)
INSERT #tree (Id, ParentId)
SELECT n.Value, CASE WHEN n.Value % 3 = 0 THEN n.Value / 4 END
FROM number n;
binki
fuente

Respuestas:

12

La respuesta de Randi Vertongen aborda correctamente cómo puede obtener el plan que desea con la versión parametrizada de la consulta. Esta respuesta complementa eso al abordar el título de la pregunta en caso de que esté interesado en los detalles.

SQL Server reescribe las expresiones de tabla común recursivas de cola (CTE) como iteración. Todo, desde Lazy Index Spool hacia abajo, es la implementación en tiempo de ejecución de la traducción iterativa. Escribí una descripción detallada de cómo funciona esta sección de un plan de ejecución en respuesta a Utilizar EXCEPTO en una expresión de tabla común recursiva .

Desea especificar un predicado (filtro) fuera del CTE y hacer que el optimizador de consultas empuje este filtro hacia abajo dentro de la recursión (reescrito como iteración) y lo aplique al miembro de anclaje. Esto significará que la recursión comienza solo con aquellos registros que coinciden ParentId = @Id.

Esta es una expectativa bastante razonable, ya sea que se use un valor literal, variable o parámetro; sin embargo, el optimizador solo puede hacer cosas para las cuales se han escrito reglas. Las reglas especifican cómo se modifica un árbol de consulta lógica para lograr una transformación particular. Incluyen lógica para garantizar que el resultado final sea seguro, es decir, devuelve exactamente los mismos datos que la especificación de consulta original en todos los casos posibles.

La regla responsable de empujar predicados en un CTE recursivo se llama SelOnIterator: una selección relacional (= predicado) en un iterador que implementa la recursividad. Más precisamente, esta regla puede copiar una selección en la parte de anclaje de la iteración recursiva:

Sel(Iter(A,R)) -> Sel(Iter(Sel(A),R))

Esta regla se puede deshabilitar con la sugerencia no documentada OPTION(QUERYRULEOFF SelOnIterator). Cuando se usa esto, el optimizador ya no puede empujar predicados con un valor literal al ancla de un CTE recursivo. No quieres eso, pero ilustra el punto.

Originalmente, esta regla se limitaba a trabajar en predicados con valores literales solamente. También se podría hacer que funcione con variables o parámetros especificando OPTION (RECOMPILE), ya que esa sugerencia habilita la optimización de incrustación de parámetros , por lo que el valor literal de tiempo de ejecución de la variable (o parámetro) se utiliza al compilar el plan. El plan no se almacena en caché, por lo que la desventaja de esto es una compilación nueva en cada ejecución.

En algún momento, la SelOnIteratorregla se mejoró para trabajar también con variables y parámetros. Para evitar cambios inesperados en el plan, esto estaba protegido bajo el indicador de seguimiento 4199, el nivel de compatibilidad de la base de datos y el nivel de compatibilidad de la revisión del optimizador de consultas. Este es un patrón bastante normal para las mejoras del optimizador, que no siempre se documentan. Las mejoras son normalmente buenas para la mayoría de las personas, pero siempre existe la posibilidad de que cualquier cambio introduzca una regresión para alguien.

Quiero poner el CTE en una vista reutilizable

Puede usar una función en línea con valores de tabla en lugar de una vista. Proporcione el valor que desea empujar hacia abajo como parámetro y coloque el predicado en el miembro de anclaje recursivo.

Si lo prefiere, habilitar la marca de seguimiento 4199 a nivel mundial también es una opción. Hay muchos cambios de optimizador cubiertos por este indicador, por lo que deberá probar cuidadosamente su carga de trabajo con esta habilitada y estar preparado para manejar las regresiones.

Paul White 9
fuente
10

Aunque en este momento no tengo el título de la revisión real, se utilizará el mejor plan de consulta al habilitar las revisiones del optimizador de consultas en su versión (SQL Server 2012).

Algunos otros métodos son:

  • Utilizando OPTION(RECOMPILE)para que el filtrado ocurra antes, en el valor literal.
  • En SQL Server 2016 o superior, las revisiones antes de esta versión se aplican automáticamente y la consulta también debe ejecutarse de manera equivalente al mejor plan de ejecución.

Revisiones optimizadoras de consultas

Puede habilitar estas correcciones con

  • Traceflag 4199 antes de SQL Server 2016
  • ALTER DATABASE SCOPED CONFIGURATION SET QUERY_OPTIMIZER_HOTFIXES=ON; a partir de SQL Server 2016. (no es necesario para su corrección)

El filtrado @idactivado se aplica antes a los miembros recursivos y de anclaje en el plan de ejecución con la revisión habilitada.

El indicador de seguimiento se puede agregar en el nivel de consulta:

OPTION(QUERYTRACEON 4199)

Al ejecutar la consulta en SQL Server 2012 SP4 GDR o SQL Server 2014 SP3 con Traceflag 4199, se elige el mejor plan de consulta:

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
    OPTION( QUERYTRACEON 4199 );

END;
GO
EXEC #c 24;

Plan de consulta en SQL Server 2014 SP3 con traceflag 4199

Plan de consulta en SQL Server 2012 SP4 GDR con traceflag 4199

Plan de consultas en SQL Server 2012 SP4 GDR sin traceflag 4199

El consenso principal es habilitar traceflag 4199 a nivel mundial cuando se utiliza una versión anterior a SQL Server 2016. Luego, queda abierto el debate sobre si habilitarlo o no. AQ / A sobre eso aquí .


Nivel de compatibilidad 130 o 140

Al probar la consulta parametrizada en una base de datos con compatibility_level= 130 o 140, el filtrado ocurre antes:

ingrese la descripción de la imagen aquí

Debido al hecho de que las correcciones 'antiguas' de traceflag 4199 están habilitadas en SQL Server 2016 y versiones posteriores.


OPCIÓN (RECOMPILAR)

Aunque se utiliza un procedimiento, SQL Server podrá filtrar el valor literal al agregar OPTION(RECOMPILE);.

ALTER PROCEDURE #c (@Id BIGINT) AS BEGIN;
    WITH descendants AS (SELECT
         t.ParentId Id
        ,t.Id DescendantId
    FROM #tree t 
    WHERE t.ParentId IS NOT NULL
    UNION ALL 
    SELECT
         d.Id
        ,t.Id DescendantId
    FROM descendants d
    JOIN #tree t ON d.DescendantId = t.ParentId)
    SELECT d.*
    FROM descendants d
    WHERE d.Id = @Id
    ORDER BY d.Id, d.DescendantId
OPTION(
RECOMPILE )

END;
GO

ingrese la descripción de la imagen aquí

Plan de consultas en SQL Server 2012 SP4 GDR con OPTION (RECOMPILE)

Randi Vertongen
fuente