Perché una scansione è più veloce della ricerca di questo predicato?


30

Sono stato in grado di riprodurre un problema di prestazioni della query che definirei inaspettato. Sto cercando una risposta incentrata sugli interni.

Sulla mia macchina, la seguente query esegue una scansione dell'indice cluster e richiede circa 6,8 secondi di tempo della 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 seguente query cerca un indice cluster (l'unica differenza è rimuovere il FORCESCANsuggerimento) ma richiede circa 18,2 secondi di tempo 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);

I piani di query sono piuttosto simili. Per entrambe le query sono presenti 120000001 righe lette dall'indice cluster:

piani di query

Sono su SQL Server 2017 CU 10. Ecco il codice per creare e popolare la two_col_key_testtabella:

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;

Spero in una risposta che vada oltre la segnalazione dello stack delle chiamate. Ad esempio, posso vedere che ci sqlmin!TCValSSInRowExprFilter<231,0,0>::GetDataXvogliono molti più cicli di CPU nella query lenta rispetto a quella veloce:

perview

Invece di fermarmi lì, vorrei capire di cosa si tratta e perché c'è una differenza così grande tra le due query.

Perché c'è una grande differenza nel tempo della CPU per queste due query?

Risposte:


31

Perché c'è una grande differenza nel tempo della CPU per queste due query?

Il piano di scansione valuta il seguente predicato non residuo trasferibile (residuo) per ogni riga:

[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'
)

scansione residua

Il piano di ricerca prevede due operazioni di ricerca:

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')

... per abbinare questa parte del predicato:

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

Un predicato residuo viene applicato alle righe che superano le condizioni di ricerca sopra (tutte le righe nell'esempio).

Tuttavia, ogni disuguaglianza è sostituita da due test separati per meno di OR maggiore di :

([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')

cercare residuo

Riscrivere ogni disuguaglianza, ad esempio:

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

... è controproducente qui. I confronti tra stringhe sensibili alle regole di confronto sono costosi. Raddoppiare il numero di confronti spiega la maggior parte della differenza nel tempo della CPU che vedi.

Puoi vederlo più chiaramente disabilitando la spinta di predicati non sargable con flag di traccia non documentato 9130. Ciò mostrerà il residuo come Filtro separato, con informazioni sulle prestazioni che puoi ispezionare separatamente:

scansione

cercare

Ciò evidenzierà anche la leggera cardinalità errata sulla ricerca, il che spiega perché l'ottimizzatore ha scelto la ricerca sulla scansione in primo luogo (si aspettava che la parte di ricerca eliminasse alcune righe).

Mentre la riscrittura della disuguaglianza può rendere possibile (possibilmente filtrata) la corrispondenza dell'indice (per sfruttare al meglio la capacità di ricerca degli indici b-tree), sarebbe meglio ripristinare successivamente questa espansione se entrambe le metà finiscono nel residuo. È possibile suggerire questo come un miglioramento nel sito di feedback di SQL Server .

Si noti inoltre che il modello di stima della cardinalità ("legacy") originale seleziona una scansione per impostazione predefinita per questa query.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.