¿Por qué un análisis es más rápido que buscar este predicado?

30

Pude reproducir un problema de rendimiento de la consulta que describiría como inesperado. Estoy buscando una respuesta que se centre en lo interno.

En mi máquina, la siguiente consulta realiza un escaneo de índice agrupado y tarda aproximadamente 6,8 segundos de tiempo de CPU:

SELECT ID1, ID2
FROM two_col_key_test WITH (FORCESCAN)
WHERE ID1 NOT IN
(
N'1', N'2',N'3', N'4', N'5',
N'6', N'7', N'8', N'9', N'10',
N'11', N'12',N'13', N'14', N'15',
N'16', N'17', N'18', N'19', N'20'
)
AND (ID1 = N'FILLER TEXT' AND ID2 >= N'' OR (ID1 > N'FILLER TEXT'))
ORDER BY ID1, ID2 OFFSET 12000000 ROWS FETCH FIRST 1 ROW ONLY
OPTION (MAXDOP 1);

La siguiente consulta realiza una búsqueda de índice agrupado (la única diferencia es eliminar la FORCESCANsugerencia) pero toma aproximadamente 18.2 segundos de tiempo de CPU:

SELECT ID1, ID2
FROM two_col_key_test
WHERE ID1 NOT IN
(
N'1', N'2',N'3', N'4', N'5',
N'6', N'7', N'8', N'9', N'10',
N'11', N'12',N'13', N'14', N'15',
N'16', N'17', N'18', N'19', N'20'
)
AND (ID1 = N'FILLER TEXT' AND ID2 >= N'' OR (ID1 > N'FILLER TEXT'))
ORDER BY ID1, ID2 OFFSET 12000000 ROWS FETCH FIRST 1 ROW ONLY
OPTION (MAXDOP 1);

Los planes de consulta son bastante similares. Para ambas consultas hay 120000001 filas leídas del índice agrupado:

planes de consulta

Estoy en SQL Server 2017 CU 10. Aquí hay un código para crear y completar la two_col_key_testtabla:

drop table if exists dbo.two_col_key_test;

CREATE TABLE dbo.two_col_key_test (
    ID1 NVARCHAR(50) NOT NULL,
    ID2 NVARCHAR(50) NOT NULL,
    FILLER NVARCHAR(50),
    PRIMARY KEY (ID1, ID2)
);

DROP TABLE IF EXISTS #t;

SELECT TOP (4000) 0 ID INTO #t
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);


INSERT INTO dbo.two_col_key_test WITH (TABLOCK)
SELECT N'FILLER TEXT' + CASE WHEN ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) > 8000000 THEN N' 2' ELSE N'' END
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, NULL
FROM #t t1
CROSS JOIN #t t2;

Espero una respuesta que haga más que solo informes de pila de llamadas. Por ejemplo, puedo ver que sqlmin!TCValSSInRowExprFilter<231,0,0>::GetDataXtoma significativamente más ciclos de CPU en la consulta lenta en comparación con la rápida:

perview

En lugar de detenerme allí, me gustaría entender qué es eso y por qué hay una diferencia tan grande entre las dos consultas.

¿Por qué hay una gran diferencia en el tiempo de CPU para estas dos consultas?

Joe Obbish
fuente

Respuestas:

31

¿Por qué hay una gran diferencia en el tiempo de CPU para estas dos consultas?

El plan de exploración evalúa el siguiente predicado no sargable (residual) insertado para cada fila:

[two_col_key_test].[ID1]<>N'1' 
AND [two_col_key_test].[ID1]<>N'10' 
AND [two_col_key_test].[ID1]<>N'11' 
AND [two_col_key_test].[ID1]<>N'12' 
AND [two_col_key_test].[ID1]<>N'13' 
AND [two_col_key_test].[ID1]<>N'14' 
AND [two_col_key_test].[ID1]<>N'15' 
AND [two_col_key_test].[ID1]<>N'16' 
AND [two_col_key_test].[ID1]<>N'17' 
AND [two_col_key_test].[ID1]<>N'18' 
AND [two_col_key_test].[ID1]<>N'19' 
AND [two_col_key_test].[ID1]<>N'2' 
AND [two_col_key_test].[ID1]<>N'20' 
AND [two_col_key_test].[ID1]<>N'3' 
AND [two_col_key_test].[ID1]<>N'4' 
AND [two_col_key_test].[ID1]<>N'5' 
AND [two_col_key_test].[ID1]<>N'6' 
AND [two_col_key_test].[ID1]<>N'7' 
AND [two_col_key_test].[ID1]<>N'8' 
AND [two_col_key_test].[ID1]<>N'9' 
AND 
(
    [two_col_key_test].[ID1]=N'FILLER TEXT' 
    AND [two_col_key_test].[ID2]>=N'' 
    OR [two_col_key_test].[ID1]>N'FILLER TEXT'
)

escaneo residual

El plan de búsqueda realiza dos operaciones de búsqueda:

Seek Keys[1]: 
    Prefix: 
    [two_col_key_test].ID1 = Scalar Operator(N'FILLER TEXT'), 
        Start: [two_col_key_test].ID2 >= Scalar Operator(N'')
Seek Keys[1]: 
    Start: [two_col_key_test].ID1 > Scalar Operator(N'FILLER TEXT')

... para que coincida con esta parte del predicado:

(ID1 = N'FILLER TEXT' AND ID2 >= N'' OR (ID1 > N'FILLER TEXT'))

Se aplica un predicado residual a las filas que pasan las condiciones de búsqueda anteriores (todas las filas en su ejemplo).

Sin embargo, cada desigualdad se reemplaza por dos pruebas separadas por menos que OR mayor que :

([two_col_key_test].[ID1]<N'1' OR [two_col_key_test].[ID1]>N'1') 
AND ([two_col_key_test].[ID1]<N'10' OR [two_col_key_test].[ID1]>N'10') 
AND ([two_col_key_test].[ID1]<N'11' OR [two_col_key_test].[ID1]>N'11') 
AND ([two_col_key_test].[ID1]<N'12' OR [two_col_key_test].[ID1]>N'12') 
AND ([two_col_key_test].[ID1]<N'13' OR [two_col_key_test].[ID1]>N'13') 
AND ([two_col_key_test].[ID1]<N'14' OR [two_col_key_test].[ID1]>N'14') 
AND ([two_col_key_test].[ID1]<N'15' OR [two_col_key_test].[ID1]>N'15') 
AND ([two_col_key_test].[ID1]<N'16' OR [two_col_key_test].[ID1]>N'16') 
AND ([two_col_key_test].[ID1]<N'17' OR [two_col_key_test].[ID1]>N'17') 
AND ([two_col_key_test].[ID1]<N'18' OR [two_col_key_test].[ID1]>N'18') 
AND ([two_col_key_test].[ID1]<N'19' OR [two_col_key_test].[ID1]>N'19') 
AND ([two_col_key_test].[ID1]<N'2' OR [two_col_key_test].[ID1]>N'2') 
AND ([two_col_key_test].[ID1]<N'20' OR [two_col_key_test].[ID1]>N'20') 
AND ([two_col_key_test].[ID1]<N'3' OR [two_col_key_test].[ID1]>N'3') 
AND ([two_col_key_test].[ID1]<N'4' OR [two_col_key_test].[ID1]>N'4') 
AND ([two_col_key_test].[ID1]<N'5' OR [two_col_key_test].[ID1]>N'5') 
AND ([two_col_key_test].[ID1]<N'6' OR [two_col_key_test].[ID1]>N'6') 
AND ([two_col_key_test].[ID1]<N'7' OR [two_col_key_test].[ID1]>N'7') 
AND ([two_col_key_test].[ID1]<N'8' OR [two_col_key_test].[ID1]>N'8') 
AND ([two_col_key_test].[ID1]<N'9' OR [two_col_key_test].[ID1]>N'9')

buscar residual

Reescribiendo cada desigualdad, por ejemplo:

[ID1] <> N'1'  ->  [ID1]<N'1' OR [ID1]>N'1'

... es contraproducente aquí. Las comparaciones de cadenas con reconocimiento de colación son caras. Duplicar el número de comparaciones explica la mayor parte de la diferencia en el tiempo de CPU que ve.

Puede ver esto más claramente deshabilitando la inserción de predicados no sargables con el indicador de traza no documentado 9130. Eso mostrará el residuo como un filtro separado, con información de rendimiento que puede inspeccionar por separado:

escanear

buscar

Esto también resaltará la ligera falta de estimación de la cardinalidad en la búsqueda, lo que explica por qué el optimizador eligió la búsqueda sobre el escaneo en primer lugar (esperaba que la porción de búsqueda eliminara algunas filas).

Si bien la reescritura de desigualdad puede hacer posible (posiblemente filtrada) la coincidencia de índices (para hacer el mejor uso de la capacidad de búsqueda de los índices b-tree), sería mejor revertir posteriormente esta expansión si ambas mitades terminan en el residuo. Puede sugerir esto como una mejora en el sitio de comentarios de SQL Server .

Tenga en cuenta también que el modelo de estimación de cardinalidad original ("heredado") selecciona una exploración por defecto para esta consulta.

Paul White dice GoFundMonica
fuente