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$1257927703
table (dans l'exemple) aura toujours plus ou égal au nombre de lignes qu'elle dbf_1162761$z$dd$1257927703
contient - 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)
Réponses:
En règle générale, SQL Server exécute les parties d'une
CASE
instruction dans l'ordre mais est libre de réorganiser lesOR
conditions. Pour certaines requêtes, vous pouvez obtenir des performances toujours meilleures en modifiant l'ordre desWHEN
expressions dans uneCASE
instruction. Parfois, vous pouvez également obtenir de meilleures performances lorsque vous modifiez l'ordre des conditions dans uneOR
instruction, 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:
Considérez la requête suivante:
Nous savons que l'évaluation de la sous-requête
X_CI
sera beaucoup moins chère que la sous-requêteX_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:
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
: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:
La requête suivante est beaucoup moins efficace:
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:
Les performances en témoignent:
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:
Et dans cette requête, ils sont évalués dans l'ordre inverse:
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
CASE
instruction ou une autre méthode pour forcer le classement. Sinon, n'hésitez pas à commander les sous-requêtes dans uneOR
condition 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:
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_HEAP
table 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.la source
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 END
pourrait ê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.