web-dev-qa-db-fra.com

Pourquoi un scan est-il plus rapide que de rechercher ce prédicat?

J'ai pu reproduire un problème de performances de requête que je décrirais comme inattendu. Je cherche une réponse centrée sur les internes.

Sur ma machine, la requête suivante effectue une analyse d'index en cluster et prend environ 6,8 secondes de temps processeur:

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 requête suivante effectue une recherche d'index clusterisé (la seule différence est la suppression de l'indication FORCESCAN) mais prend environ 18,2 secondes de temps processeur:

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

Les plans de requête sont assez similaires. Pour les deux requêtes, 120000001 lignes sont lues à partir de l'index cluster:

query plans

Je suis sur SQL Server 2017 CU 10. Voici le code pour créer et remplir le two_col_key_test table:

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;

J'espère une réponse qui fait plus que des rapports de pile d'appels. Par exemple, je peux voir que sqlmin!TCValSSInRowExprFilter<231,0,0>::GetDataX prend beaucoup plus de cycles CPU dans la requête lente par rapport à la requête rapide:

perview

Au lieu de m'arrêter là, j'aimerais comprendre ce que c'est et pourquoi il y a une si grande différence entre les deux requêtes.

Pourquoi y a-t-il une grande différence de temps CPU pour ces deux requêtes?

30
Joe Obbish

Pourquoi y a-t-il une grande différence de temps CPU pour ces deux requêtes?

Le plan d'analyse évalue le prédicat non résolu (résiduel) poussé suivant pour chaque ligne:

[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 residual

Le plan de recherche effectue deux opérations de recherche:

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

... pour faire correspondre cette partie du prédicat:

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

Un prédicat résiduel est appliqué aux lignes qui remplissent les conditions de recherche ci-dessus (toutes les lignes dans votre exemple).

Cependant, chaque inégalité est remplacée par deux tests distincts pour inférieur à OR supérieur à :

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

seek residual

Réécrire chaque inégalité par exemple:

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

... est contre-productif ici. Les comparaisons de chaînes sensibles au classement sont coûteuses. Le doublement du nombre de comparaisons explique la majeure partie de la différence de temps CPU que vous voyez.

Vous pouvez le voir plus clairement en désactivant la transmission des prédicats non négociables avec l'indicateur de trace non documenté 9130. Cela affichera le résiduel comme un filtre distinct, avec des informations sur les performances que vous pouvez inspecter séparément:

scan

seek

Cela mettra également en évidence la légère cardinalité mal estimée sur la recherche, ce qui explique pourquoi l'optimiseur a choisi la recherche plutôt que l'analyse (il s'attendait à ce que la partie de recherche élimine certaines lignes).

Bien que la réécriture des inégalités puisse rendre possible (éventuellement filtré) la correspondance d'index (pour tirer le meilleur parti de la capacité de recherche des index b-tree), il serait préférable de revenir ultérieurement sur cette expansion si les deux moitiés se retrouvent dans le résidu. Vous pouvez suggérer cela comme une amélioration sur le site de commentaires SQL Server .

Notez également que le modèle d'estimation de cardinalité d'origine ("hérité") sélectionne un scan par défaut pour cette requête.

32
Paul White 9