Excesiva concesión de memoria de clasificación

45

¿Por qué esta simple consulta tiene tanta memoria?

-- Demo table
CREATE TABLE dbo.Test
(
    TID integer IDENTITY NOT NULL,
    FilterMe integer NOT NULL,
    SortMe integer NOT NULL,
    Unused nvarchar(max) NULL,

    CONSTRAINT PK_dbo_Test_TID
    PRIMARY KEY CLUSTERED (TID)
);
GO
-- 100,000 example rows
INSERT dbo.Test WITH (TABLOCKX)
    (FilterMe, SortMe)
SELECT TOP (100 * 1000)
    CHECKSUM(NEWID()) % 1000,
    CHECKSUM(NEWID())
FROM sys.all_columns AS AC1
CROSS JOIN sys.all_columns AS AC2;
GO    
-- Query
SELECT
    T.TID,
    T.FilterMe,
    T.SortMe,
    T.Unused
FROM dbo.Test AS T 
WHERE 
    T.FilterMe = 567
ORDER BY 
    T.SortMe;

Para un estimado de 50 filas, el optimizador reserva casi 500 MB para el tipo:

Plan estimado

Paul White
fuente

Respuestas:

42

Este es un error en SQL Server (de 2008 a 2014 inclusive).

Mi informe de error está aquí .

La condición de filtrado se introduce en el operador de exploración como un predicado residual, pero la memoria otorgada para la clasificación se calcula erróneamente en función de la estimación de la cardinalidad del prefiltro .

Para ilustrar el problema, podemos usar el indicador de traza 9130 (no documentado y no compatible) para evitar que el filtro sea empujado hacia el operador de exploración . La memoria otorgada a la clasificación ahora se basa correctamente en la cardinalidad estimada de la salida del filtro, no en la exploración:

SELECT
    T.TID,
    T.FilterMe,
    T.SortMe,
    T.Unused
FROM dbo.Test AS T 
WHERE 
    T.FilterMe = 567
ORDER BY 
    T.SortMe
OPTION (QUERYTRACEON 9130); -- Not for production systems!

Plan estimado

Para un sistema de producción , se deberán tomar medidas para evitar la forma problemática del plan (un filtro insertado en un escaneo con una clasificación en otra columna). Una forma de hacerlo es proporcionar un índice sobre la condición del filtro y / o proporcionar el orden de clasificación requerido.

-- Index on the filter condition only
CREATE NONCLUSTERED INDEX IX_dbo_Test_FilterMe
ON dbo.Test (FilterMe);

Con este índice en su lugar, la concesión de memoria deseada para la clasificación es de solo 928 KB :

Con índice de filtro

Yendo más lejos, el siguiente índice puede evitar la clasificación por completo ( concesión de memoria cero ):

-- Provides filtering and sort order
-- nvarchar(max) column deliberately not INCLUDEd
CREATE NONCLUSTERED INDEX IX_dbo_Test_FilterMe_SortMe
ON dbo.Test (FilterMe, SortMe);

Con filtro e índice de clasificación

Probado y con error confirmado en las siguientes compilaciones de SQL Server x64 Developer Edition:

2014   : 12.00.2430 (RTM CU4)
2012   : 11.00.5556 (SP2 CU3)
2008R2 : 10.50.6000 (SP3)
2008   : 10.00.6000 (SP4)

Esto se solucionó en SQL Server 2016 Service Pack 1 . Las notas de la versión incluyen lo siguiente:

El número de error VSTS 8024987 Los
escaneos de tablas y los escaneos de índice con predicado push down tienden a sobreestimar la concesión de memoria para el operador principal.

Probado y confirmado fijo en:

  • Microsoft SQL Server 2016 (SP1) - 13.0.4001.0 (X64) Developer Edition
  • Microsoft SQL Server 2014 (SP2-CU3) 12.0.5538.0 (X64) Developer Edition

Ambos modelos CE.

Paul White
fuente
5

Desde SQL 2012 en adelante, podría buscar una gran discrepancia entre SerialRequiredMemoryy SerialDesiredMemory, por ejemplo, algo como esto:

-- Search plan cache for Memory Grant issues
IF OBJECT_ID('tempdb..#tmp') IS NOT NULL DROP TABLE #tmp

-- Collect more info about the plan here if required, eg usecounts, objtype etc, 
SELECT IDENTITY( INT, 1, 1 ) rowId, query_plan
INTO #tmp
FROM sys.dm_exec_cached_plans cp WITH(NOLOCK)
    CROSS APPLY sys.dm_exec_query_plan(plan_handle)
GO


;WITH cte AS
(
SELECT
    rowId,
    query_plan,
    m.c.value ('@SerialRequiredMemory', 'INT' ) AS SerialRequiredMemory,
    m.c.value ('@SerialDesiredMemory', 'INT' ) AS SerialDesiredMemory

FROM #tmp t
    CROSS APPLY t.query_plan.nodes ( '//*:MemoryGrantInfo[@SerialDesiredMemory[. > 0]]' ) m(c)
), cte2 AS (
SELECT *,
    CAST( CAST( SerialDesiredMemory AS DECIMAL(10,2) ) / CAST( SerialRequiredMemory AS DECIMAL(10,2) ) AS DECIMAL(10,2) ) Desired_to_Required_ratio
FROM cte
)
SELECT TOP 20
    rowId,
    query_plan,
    SerialRequiredMemory SerialRequiredMemory_KB,
    SerialDesiredMemory SerialDesiredMemory_KB,
    CAST( SerialRequiredMemory / 1024. AS DECIMAL(10,2) ) SerialRequiredMemory_MB,
    CAST( SerialDesiredMemory / 1024. AS DECIMAL(10,2) ) SerialDesiredMemory_MB,
    Desired_to_Required_ratio
FROM cte2
WHERE Desired_to_Required_ratio > 100
ORDER BY Desired_to_Required_ratio DESC

Algunas notas adicionales sobre estos nuevos atributos aquí . Esta consulta es un poco aproximada y está lista, pero recogió la consulta de clasificación excesiva de mi cuadro de desarrollo de SQL Server 2014 con una proporción de 975.47 más un par de otros planes alucinantes. La relación 'normal' (al menos de mis pruebas limitadas) parece ser ~ 1.

HTH

wBob
fuente
3

Gracias por toda la ayuda. Pensé que enviaría una versión actualizada de la consulta anterior que encontramos útil.

-- Search plan cache for Memory Grant issues
IF OBJECT_ID('tempdb..#tmp') IS NOT NULL DROP TABLE #tmp

-- Collect more info about the plan here if required, eg usecounts, objtype etc, 
SELECT IDENTITY( INT, 1, 1 ) rowId, query_plan, db = DB_NAME(CAST(pa.value AS int))
INTO #tmp
FROM sys.dm_exec_cached_plans cp WITH(NOLOCK)
    CROSS APPLY sys.dm_exec_query_plan(cp.plan_handle)
    OUTER APPLY sys.dm_exec_plan_attributes(cp.plan_handle) pa 
    WHERE pa.attribute = 'dbid' 
GO

;WITH cte AS
(
SELECT
    rowId,
    query_plan,
    m.c.value ('@SerialRequiredMemory', 'INT' ) AS SerialRequiredMemory,
    m.c.value ('@SerialDesiredMemory', 'INT' ) AS SerialDesiredMemory,
    db
FROM #tmp t
    CROSS APPLY t.query_plan.nodes ( '//*:MemoryGrantInfo[@SerialDesiredMemory[. > 0]]' ) m(c)
), cte2 AS (
SELECT *,
    CAST( CAST( SerialDesiredMemory AS DECIMAL(10,2) ) / CAST( SerialRequiredMemory AS DECIMAL(10,2) ) AS DECIMAL(10,2) ) Desired_to_Required_ratio
FROM cte
)
SELECT TOP 20
    rowId,
    query_plan,
    SerialRequiredMemory SerialRequiredMemory_KB,
    SerialDesiredMemory SerialDesiredMemory_KB,
    CAST( SerialRequiredMemory / 1024. AS DECIMAL(10,2) ) SerialRequiredMemory_MB,
    CAST( SerialDesiredMemory / 1024. AS DECIMAL(10,2) ) SerialDesiredMemory_MB,
    Desired_to_Required_ratio,
    db
FROM cte2
WHERE Desired_to_Required_ratio > 100
ORDER BY Desired_to_Required_ratio DESC
Doug B
fuente