ordre des clauses dans «EXISTE (…) OU EXISTE (…)»

11

J'ai une classe de requêtes qui testent l'existence de l'une des deux choses. C'est de la forme

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM ...)
  OR EXISTS (SELECT 1 FROM ...)
THEN 1 ELSE 0 END;

L'instruction réelle est générée en C et exécutée en tant que requête ad hoc sur une connexion ODBC.

Il est récemment apparu que le deuxième SELECT sera probablement plus rapide que le premier SELECT dans la plupart des cas et que le changement de l'ordre des deux clauses EXISTS a provoqué une accélération drastique dans au moins un cas de test abusif que nous venions de créer.

La chose évidente à faire est simplement d'aller de l'avant et de changer les deux clauses, mais je voulais voir si quelqu'un plus familier avec SQL Server voudrait peser là-dessus. J'ai l'impression de compter sur une coïncidence et un "détail de mise en œuvre".

(Il semble également que si SQL Server était plus intelligent, il exécuterait les deux clauses EXISTS en parallèle et laisserait celui qui terminerait le premier court-circuiter l'autre.)

Existe-t-il un meilleur moyen d'obtenir de SQL Server une amélioration constante du temps d'exécution d'une telle requête?

Mise à jour

Merci pour votre temps et votre intérêt pour ma question. Je ne m'attendais pas à des questions sur les plans de requête réels, mais je suis prêt à les partager.

Il s'agit d'un composant logiciel qui prend en charge SQL Server 2008R2 et versions ultérieures. La forme des données peut être très différente selon la configuration et l'utilisation. Mon collègue a pensé à apporter cette modification à la requête parce que la dbf_1162761$z$rv$1257927703table (dans l'exemple) aura toujours plus ou égal au nombre de lignes qu'elle dbf_1162761$z$dd$1257927703contient - parfois beaucoup plus (ordres de grandeur).

Voici le cas abusif que j'ai mentionné. La première requête est lente et prend environ 20 secondes. La deuxième requête se termine en un instant.

Pour ce que ça vaut, le bit "OPTIMISER POUR INCONNU" a également été ajouté récemment car le reniflage de paramètres mettait à la poubelle certains cas.

Requête d'origine:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

Plan d'origine:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)

Requête fixe:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$dd$1257927703 dd INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=dd.txid WHERE tx.generation BETWEEN 1500 AND 2502)
  OR EXISTS (SELECT 1 FROM zumero.dbf_1162761$z$rv$1257927703 rv INNER JOIN zumero.dbf_1162761$t$tx tx ON tx.txid=rv.txid WHERE tx.generation BETWEEN 1500 AND 2502)
THEN 1 ELSE 0 END
OPTION (OPTIMIZE FOR UNKNOWN)

Plan fixe:

|--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [Expr1007] THEN (1) ELSE (0) END))
     |--Nested Loops(Left Semi Join, DEFINE:([Expr1007] = [PROBE VALUE]))
          |--Constant Scan
          |--Concatenation
               |--Nested Loops(Inner Join, OUTER REFERENCES:([tx].[txid]))
               |    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[PK__dbf_1162__E3BA953EC2197789] AS [tx]),  WHERE:([scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]>=(1500) AND [scale].[zumero].[dbf_1162761$t$tx].[generation] as [tx].[generation]<=(2502)) ORDERED FORWARD)
               |    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[n$dbf_1162761$z$dd$txid$1257927703] AS [dd]), SEEK:([dd].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]),  WHERE:([scale].[zumero].[dbf_1162761$z$dd$1257927703].[txid] as [dd].[txid]>(0)) ORDERED FORWARD)
               |--Nested Loops(Inner Join, WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]=[scale].[zumero].[dbf_1162761$t$tx].[txid] as [tx].[txid]))
                    |--Clustered Index Scan(OBJECT:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[PK__dbf_1162__97770A2F62EEAE79] AS [rv]), WHERE:([scale].[zumero].[dbf_1162761$z$rv$1257927703].[txid] as [rv].[txid]>(0)))
                    |--Index Seek(OBJECT:([scale].[zumero].[dbf_1162761$t$tx].[gendex] AS [tx]), SEEK:([tx].[generation] >= (1500) AND [tx].[generation] <= (2502)) ORDERED FORWARD)
jr
la source

Réponses:

11

En règle générale, SQL Server exécute les parties d'une CASEinstruction dans l'ordre mais est libre de réorganiser les ORconditions. Pour certaines requêtes, vous pouvez obtenir des performances toujours meilleures en modifiant l'ordre des WHENexpressions dans une CASEinstruction. Parfois, vous pouvez également obtenir de meilleures performances lorsque vous modifiez l'ordre des conditions dans une ORinstruction, mais ce n'est pas un comportement garanti.

Il est probablement préférable de le parcourir avec un exemple simple. Je teste contre SQL Server 2016, il est donc possible que vous n'obtiendrez pas exactement les mêmes résultats sur votre machine, mais pour autant que je sache, les mêmes principes s'appliquent. Je vais d'abord mettre un million d'entiers de 1 à 1000000 dans deux tables, une avec un index clusterisé et l'autre comme un tas:

CREATE TABLE dbo.X_HEAP (ID INT NOT NULL, FLUFF VARCHAR(100));

INSERT INTO dbo.X_HEAP  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

CREATE TABLE dbo.X_CI (ID INT NOT NULL, FLUFF VARCHAR(100), PRIMARY KEY (ID));

INSERT INTO dbo.X_CI  WITH (TABLOCK)
SELECT TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

Considérez la requête suivante:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

Nous savons que l'évaluation de la sous-requête X_CIsera beaucoup moins chère que la sous-requête X_HEAP, en particulier lorsqu'il n'y a pas de ligne correspondante. S'il n'y a pas de ligne correspondante, il suffit de faire quelques lectures logiques par rapport à la table avec un index clusterisé. Cependant, nous aurions besoin d'analyser toutes les lignes du tas pour savoir qu'il n'y a pas de ligne correspondante. L'optimiseur le sait aussi. De manière générale, l'utilisation d'un index clusterisé pour rechercher une ligne est très bon marché par rapport à l'analyse d'une table.

Pour cet exemple de données, j'écrirais la requête comme ceci:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
ELSE 0 END;

Cela oblige effectivement SQL Server à exécuter la sous-requête sur la table avec un index cluster en premier. Voici les résultats de SET STATISTICS IO, TIME ON:

Tableau 'X_CI'. Nombre de balayages 0, lectures logiques 3, lectures physiques 0

Temps d'exécution SQL Server: temps CPU = 0 ms, temps écoulé = 0 ms.

En regardant le plan de requête, si la recherche sur l'étiquette 1 renvoie des données, l'analyse sur l'étiquette 2 n'est pas requise et ne se produira pas:

bonne requête

La requête suivante est beaucoup moins efficace:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000) THEN 1 
ELSE 0 END
OPTION (MAXDOP 1);

En regardant le plan de requête, nous voyons que l'analyse à l'étiquette 2 se produit toujours. Si une ligne est trouvée, la recherche sur l'étiquette 1 est ignorée. Ce n'est pas l'ordre que nous voulions:

mauvais plan de requête

Les performances en témoignent:

Tableau 'X_HEAP'. Nombre de scans 1, lectures logiques 7247

Temps d'exécution SQL Server: temps CPU = 15 ms, temps écoulé = 22 ms.

Pour revenir à la requête d'origine, pour cette requête, je vois la recherche et l'analyse évaluées dans l'ordre qui est bon pour les performances:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
THEN 1 ELSE 0 END;

Et dans cette requête, ils sont évalués dans l'ordre inverse:

SELECT CASE
  WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000)
  OR EXISTS (SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000)
THEN 1 ELSE 0 END;

Cependant, contrairement à la paire de requêtes précédente, rien n'oblige l'optimiseur de requêtes SQL Server à évaluer l'une avant l'autre. Vous ne devez pas vous fier à ce comportement pour quelque chose d'important.

En conclusion, si vous avez besoin qu'une sous-requête soit évaluée avant l'autre, utilisez une CASEinstruction ou une autre méthode pour forcer le classement. Sinon, n'hésitez pas à commander les sous-requêtes dans une ORcondition comme vous le souhaitez, mais sachez qu'il n'y a aucune garantie que l'optimiseur les exécutera dans l'ordre tel qu'il est écrit.

Addenda:

Une question de suivi naturelle est que pouvez-vous faire si vous voulez que SQL Server décide quelle requête est la moins chère et exécute celle-ci en premier? Jusqu'à présent, toutes les méthodes semblent être implémentées par SQL Server dans l'ordre d'écriture de la requête, même si le comportement n'est pas garanti pour certaines d'entre elles.

Voici une option qui semble fonctionner pour les simples tables de démonstration:

SELECT CASE
  WHEN EXISTS (
    SELECT 1
    FROM (
        SELECT TOP 2 1 t
        FROM 
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_HEAP 
            WHERE ID = 50000 
        ) h
        CROSS JOIN
        (
            SELECT 1 ID

            UNION ALL

            SELECT TOP 1 ID 
            FROM dbo.X_CI
            WHERE ID = 50000
        ) ci
    ) cnt
    HAVING COUNT(*) = 2
)
THEN 1 ELSE 0 END;

Vous pouvez trouver une démo de violon db ici . La modification de l'ordre des tables dérivées ne modifie pas le plan de requête. Dans les deux requêtes, la X_HEAPtable n'est pas touchée. En d'autres termes, l'optimiseur de requêtes semble exécuter la requête la moins chère en premier. Je ne peux pas recommander d'utiliser quelque chose comme ça dans la production, c'est donc principalement pour la valeur de la curiosité. Il peut y avoir un moyen beaucoup plus simple d'accomplir la même chose.

Joe Obbish
la source
4
Ou CASE WHEN EXISTS (SELECT 1 FROM dbo.X_CI WHERE ID = 500000 UNION ALL SELECT 1 FROM dbo.X_HEAP WHERE ID = 500000) THEN 1 ELSE 0 ENDpourrait être une alternative, bien que cela dépende toujours de décider manuellement quelle requête est la plus rapide et de la placer en premier. Je ne sais pas s'il existe un moyen de l'exprimer de sorte que SQL Server se réorganise automatiquement afin que le bon marché soit automatiquement évalué en premier.
Martin Smith