Warum ist ein Scan schneller als die Suche nach diesem Prädikat?

30

Ich konnte ein Problem mit der Abfrageleistung reproduzieren, das ich als unerwartet beschreiben würde. Ich suche nach einer Antwort, die sich auf Interna konzentriert.

Auf meinem Computer führt die folgende Abfrage einen Clustered-Index-Scan durch und benötigt ca. 6,8 Sekunden CPU-Zeit:

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

Die folgende Abfrage führt eine Clustered-Index-Suche durch (der einzige Unterschied besteht darin, dass der FORCESCANHinweis entfernt wird), benötigt jedoch ungefähr 18,2 Sekunden CPU-Zeit:

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

Die Abfragepläne sind ziemlich ähnlich. Für beide Abfragen werden 120000001 Zeilen aus dem Clustered-Index gelesen:

Abfragepläne

Ich bin auf SQL Server 2017 CU 10. Hier ist Code zum Erstellen und Auffüllen der two_col_key_testTabelle:

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;

Ich hoffe auf eine Antwort, die mehr kann als nur das Call-Stack-Reporting. Ich sehe zum Beispiel, dass sqlmin!TCValSSInRowExprFilter<231,0,0>::GetDataXdie langsame Abfrage wesentlich mehr CPU-Zyklen benötigt als die schnelle:

übersicht

Anstatt dort anzuhalten, möchte ich verstehen, was das ist und warum es einen so großen Unterschied zwischen den beiden Abfragen gibt.

Warum gibt es einen großen Unterschied in der CPU-Zeit für diese beiden Abfragen?

Joe Obbish
quelle

Antworten:

31

Warum gibt es einen großen Unterschied in der CPU-Zeit für diese beiden Abfragen?

Der Scan-Plan wertet für jede Zeile das folgende nicht aufladbare (verbleibende) Prädikat aus:

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

Scan-Rest

Der Suchplan führt zwei Suchvorgänge aus:

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

... um diesen Teil des Prädikats abzugleichen:

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

Ein Residuum-Prädikat wird auf Zeilen angewendet, die die obigen Suchbedingungen erfüllen (alle Zeilen in Ihrem Beispiel).

Jede Ungleichung wird jedoch durch zwei separate Tests für weniger als OR größer ersetzt als :

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

Rest suchen

Umschreiben jeder Ungleichung, zB:

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

... ist hier kontraproduktiv. Kollationsbewusste Zeichenfolgenvergleiche sind teuer. Die Verdoppelung der Anzahl der Vergleiche erklärt den größten Unterschied in der CPU-Zeit, die Sie sehen.

Sie können dies deutlicher erkennen, indem Sie das Pushen von nicht aufladbaren Prädikaten mit dem Flag 9130 für nicht dokumentierte Ablaufverfolgung deaktivieren. Dadurch wird das Residuum als separater Filter mit Leistungsinformationen angezeigt, die Sie separat überprüfen können:

Scan

suchen

Dies wird auch die leichte Kardinalitätsfehlschätzung bei der Suche hervorheben, die erklärt, warum das Optimierungsprogramm die Suche zuerst über den Scan ausgewählt hat (es wurde erwartet, dass der Suchabschnitt einige Zeilen eliminiert).

Während das Neuschreiben der Ungleichheit möglicherweise eine (möglicherweise gefilterte) Indexübereinstimmung ermöglicht (um die Suchfähigkeit von B-Baum-Indizes bestmöglich auszunutzen), ist es besser, diese Erweiterung anschließend rückgängig zu machen, wenn beide Hälften im Residuum enden. Sie können dies als Verbesserung der SQL Server-Feedback-Site vorschlagen .

Beachten Sie auch, dass das ursprüngliche Modell zur Schätzung der Kardinalität ("Legacy") standardmäßig einen Scan für diese Abfrage auswählt.

Paul White sagt GoFundMonica
quelle