¿Por qué SQL Server usa un mejor plan de ejecución cuando alineo la variable?

32

Tengo una consulta SQL que estoy tratando de optimizar:

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'

SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

MyTable tiene dos índices:

CREATE NONCLUSTERED INDEX IX_MyTable_SomeTimestamp_Includes
ON dbo.MyTable (SomeTimestamp ASC)
INCLUDE(Id, SomeInt)

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp)

Cuando ejecuto la consulta exactamente como se escribió anteriormente, SQL Server escanea el primer índice, lo que resulta en 189,703 lecturas lógicas y una duración de 2-3 segundos.

Cuando alineo la @Idvariable y ejecuto la consulta nuevamente, SQL Server busca el segundo índice, lo que resulta en solo 104 lecturas lógicas y una duración de 0.001 segundos (básicamente instantánea).

Necesito la variable, pero quiero que SQL use el buen plan. Como solución temporal pongo una pista de índice en la consulta, y la consulta es básicamente instantánea. Sin embargo, trato de mantenerme alejado de las sugerencias de índice cuando sea posible. Por lo general, supongo que si el optimizador de consultas no puede hacer su trabajo, entonces hay algo que puedo hacer (o dejar de hacer) para ayudarlo sin decirle explícitamente qué hacer.

Entonces, ¿por qué SQL Server tiene un mejor plan cuando incluyo la variable?

Perno de lluvia
fuente

Respuestas:

44

En SQL Server, hay tres formas comunes de predicado sin unión:

Con un valor literal :

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = 1;

Con un parámetro :

CREATE PROCEDURE dbo.SomeProc(@Reputation INT)
AS
BEGIN
    SELECT COUNT(*) AS records
    FROM   dbo.Users AS u
    WHERE  u.Reputation = @Reputation;
END;

Con una variable local :

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

Resultados

Cuando utiliza un valor literal , y su plan no es a) Trivial yb) Simple parametrizado o c) no tiene activada la parametrización forzada , el optimizador crea un plan muy especial solo para ese valor.

Cuando usa un parámetro , el optimizador creará un plan para ese parámetro (esto se llama detección de parámetros ) y luego reutilizará ese plan, en ausencia de sugerencias de recompilación, desalojo de caché del plan, etc.

Cuando usa una variable local , el optimizador hace un plan para ... Algo .

Si tuviera que ejecutar esta consulta:

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

El plan se vería así:

NUECES

Y el número estimado de filas para esa variable local se vería así:

NUECES

Aunque la consulta devuelve un recuento de 4.744.427.

Las variables locales, al ser desconocidas, no usan la parte 'buena' del histograma para la estimación de la cardinalidad. Usan una suposición basada en el vector de densidad.

NUECES

SELECT 5.280389E-05 * 7250739 AS [poo]

Eso te dará 382.86722457471, que es la conjetura que hace el optimizador.

Estas suposiciones desconocidas suelen ser muy malas, y a menudo pueden conducir a malos planes y malas elecciones de índice.

¿Arreglando lo?

Sus opciones generalmente son:

  • Indicios frágiles del índice
  • Sugerencias de recompilación potencialmente caras
  • SQL dinámico parametrizado
  • Un procedimiento almacenado
  • Mejora el índice actual

Sus opciones específicamente son:

Mejorar el índice actual significa extenderlo para cubrir todas las columnas que necesita la consulta:

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp, SomeTimestamp, SomeInt)
WITH (DROP_EXISTING = ON);

Asumiendo que los Idvalores son razonablemente selectivos, esto le dará un buen plan y ayudará al optimizador al darle un método de acceso a datos 'obvio'.

Más lectura

Puede leer más sobre la incrustación de parámetros aquí:

Erik Darling
fuente
12

Asumiré que tiene datos asimétricos, que no desea utilizar sugerencias de consulta para forzar al optimizador a qué hacer y que necesita obtener un buen rendimiento para todos los valores de entrada posibles @Id. Puede obtener un plan de consulta que solo requiere unos pocos puñados de lecturas lógicas para cualquier valor de entrada posible si está dispuesto a crear el siguiente par de índices (o su equivalente):

CREATE INDEX GetMinSomeTimestamp ON dbo.MyTable (Id, SomeTimestamp) WHERE SomeBit = 1;
CREATE INDEX GetMaxSomeInt ON dbo.MyTable (Id, SomeInt) WHERE SomeBit = 1;

A continuación se muestran mis datos de prueba. Puse 13 M filas en la tabla e hice que la mitad de ellas tuviera un valor '3A35EA17-CE7E-4637-8319-4C517B6E48CA'para la Idcolumna.

DROP TABLE IF EXISTS dbo.MyTable;

CREATE TABLE dbo.MyTable (
    Id uniqueidentifier,
    SomeTimestamp DATETIME2,
    SomeInt INT,
    SomeBit BIT,
    FILLER VARCHAR(100)
);

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT NEWID(), CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT '3A35EA17-CE7E-4637-8319-4C517B6E48CA', CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

Esta consulta puede parecer un poco extraña al principio:

DECLARE @Id UNIQUEIDENTIFIER = '3A35EA17-CE7E-4637-8319-4C517B6E48CA'

SELECT
  @Id,
  st.SomeTimestamp,
  si.SomeInt
FROM (
    SELECT TOP (1) SomeInt, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeInt DESC
) si
CROSS JOIN (
    SELECT TOP (1) SomeTimestamp, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeTimestamp ASC
) st;

Está diseñado para aprovechar el orden de los índices para encontrar el valor mínimo o máximo con algunas lecturas lógicas. El CROSS JOINestá ahí para obtener resultados correctos cuando no hay filas coincidentes para el @Idvalor. Incluso si filtro en el valor más popular en la tabla (que coincide con 6.5 millones de filas) solo obtengo 8 lecturas lógicas:

Tabla 'MyTable'. Escaneo recuento 2, lecturas lógicas 8

Aquí está el plan de consulta:

ingrese la descripción de la imagen aquí

Ambos índices buscan encontrar 0 o 1 filas. Es extremadamente eficiente, pero crear dos índices puede ser excesivo para su escenario. En su lugar, puede considerar el siguiente índice:

CREATE INDEX CoveringIndex ON dbo.MyTable (Id) INCLUDE (SomeTimestamp, SomeInt) WHERE SomeBit = 1;

Ahora el plan de consulta para la consulta original (con una MAXDOP 1pista opcional ) se ve un poco diferente:

ingrese la descripción de la imagen aquí

Las búsquedas clave ya no son necesarias. Con una mejor ruta de acceso que debería funcionar bien para todas las entradas, no debería tener que preocuparse de que el optimizador elija el plan de consulta incorrecto debido al vector de densidad. Sin embargo, esta consulta e índice no serán tan eficientes como la otra si busca un @Idvalor popular .

Tabla 'MyTable'. Escaneo recuento 1, lecturas lógicas 33757

Joe Obbish
fuente
2

No puedo responder por qué aquí, pero la forma rápida y sucia de garantizar que la consulta se ejecute de la manera deseada es:

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'
SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable WITH (INDEX(IX_MyTable_Id_SomeBit_Includes))
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

Esto conlleva el riesgo de que la tabla o los índices cambien en el futuro, de modo que esta optimización se vuelva disfuncional, pero está disponible si la necesita. Esperemos que alguien pueda ofrecerle una respuesta de causa raíz, como solicitó, en lugar de esta solución alternativa.

Jon de todos los oficios
fuente