SQL Server divise A <> B en A <B OU A> B, produisant des résultats étranges si B n'est pas déterministe


26

Nous avons rencontré un problème intéressant avec SQL Server. Prenons l'exemple de repro suivant:

CREATE TABLE #test (s_guid uniqueidentifier PRIMARY KEY);
INSERT INTO #test (s_guid) VALUES ('7E28EFF8-A80A-45E4-BFE0-C13989D69618');

SELECT s_guid FROM #test
WHERE s_guid = '7E28EFF8-A80A-45E4-BFE0-C13989D69618'
  AND s_guid <> NEWID();

DROP TABLE #test;

violon

Veuillez oublier un instant que la s_guid <> NEWID()condition semble entièrement inutile - ce n'est qu'un exemple de repro minimal. Étant donné que la probabilité de NEWID()correspondre à une valeur constante donnée est extrêmement faible, elle devrait être évaluée à VRAI à chaque fois.

Mais ce n'est pas le cas. L'exécution de cette requête renvoie généralement 1 ligne, mais parfois (assez fréquemment, plus d'une fois sur 10) renvoie 0 ligne. Je l'ai reproduit avec SQL Server 2008 sur mon système, et vous pouvez le reproduire en ligne avec le violon lié ci-dessus (SQL Server 2014).

L'examen du plan d'exécution révèle que l'analyseur de requêtes divise apparemment la condition en s_guid < NEWID() OR s_guid > NEWID():

capture d'écran du plan de requête

... ce qui explique complètement pourquoi il échoue parfois (si le premier ID généré est plus petit et le second plus grand que l'ID donné).

SQL Server est-il autorisé à évaluer en A <> Btant que A < B OR A > B, même si l'une des expressions n'est pas déterministe? Si oui, où est-il documenté? Ou avons-nous trouvé un bug?

Fait intéressant, AND NOT (s_guid = NEWID())donne le même plan d'exécution (et le même résultat aléatoire).

Nous avons constaté ce problème lorsqu'un développeur souhaitait éventuellement exclure une ligne particulière et utilisait:

s_guid <> ISNULL(@someParameter, NEWID())

comme "raccourci" pour:

(@someParameter IS NULL OR s_guid <> @someParameter)

Je recherche de la documentation et / ou la confirmation d'un bug. Le code n'est pas tout à fait pertinent, donc des solutions de contournement ne sont pas nécessaires.


Réponses:


22

SQL Server est-il autorisé à évaluer en A <> Btant que A < B OR A > B, même si l'une des expressions n'est pas déterministe?

C'est un point quelque peu controversé, et la réponse est un "oui" nuancé.

La meilleure discussion dont j'ai connaissance a été donnée en réponse au rapport de bogue Connect d'Itzik Ben-Gan, Bug avec NEWID et Expressions de table , qui a été fermé car il ne sera pas corrigé. Connect a depuis été retiré, donc le lien est là vers une archive Web. Malheureusement, beaucoup de matériel utile a été perdu (ou rendu plus difficile à trouver) par la disparition de Connect. Quoi qu'il en soit, les citations les plus utiles de Jim Hogg de Microsoft sont:

Cela touche au cœur même du problème - l'optimisation est-elle autorisée à modifier la sémantique d'un programme? C'est-à-dire: si un programme donne certaines réponses, mais s'exécute lentement, est-il légitime qu'un Query Optimizer accélère l'exécution de ce programme, tout en modifiant également les résultats fournis?

Avant de crier "NON!" (ma propre inclination personnelle aussi :-), considérez: la bonne nouvelle est que, dans 99% des cas, les réponses SONT les mêmes. L'optimisation des requêtes est donc clairement une victoire. La mauvaise nouvelle est que, si la requête contient du code à effet secondaire, différents plans PEUVENT en effet produire des résultats différents. Et NEWID () est une de ces «fonctions» à effets secondaires (non déterministes) qui expose la différence. [En fait, si vous expérimentez, vous pouvez en concevoir d'autres - par exemple, l'évaluation en court-circuit des clauses AND: faites en sorte que la deuxième clause lance une division arithmétique divisée par zéro - différentes optimisations peuvent exécuter cette deuxième clause AVANT la première clause] Explication de Craig, ailleurs dans ce fil, que SqlServer ne garantit pas lorsque les opérateurs scalaires sont exécutés.

Nous avons donc le choix: si nous voulons garantir un certain comportement en présence de code non déterministe (à effets secondaires) - de sorte que les résultats de JOIN, par exemple, suivent la sémantique d'une exécution en boucle imbriquée - alors nous peut utiliser des OPTIONS appropriées pour forcer ce comportement - comme le souligne UC. Mais le code résultant fonctionnera lentement - c'est le coût de l'entrave à l'optimiseur de requête.

Cela dit, nous déplaçons l'Optimiseur de requête dans le sens d'un comportement "comme prévu" pour NEWID () - en échangeant les performances contre des "résultats attendus".

Un exemple du changement de comportement à cet égard au fil du temps est NULLIF fonctionne incorrectement avec des fonctions non déterministes telles que RAND () . Il existe également d'autres cas similaires utilisant par exemple COALESCEune sous-requête qui peuvent produire des résultats inattendus et qui sont également traités progressivement.

Jim poursuit:

Fermer la boucle . . . J'ai discuté de cette question avec l'équipe de développement. Et finalement, nous avons décidé de ne pas changer le comportement actuel, pour les raisons suivantes:

1) L'optimiseur ne garantit pas le timing ou le nombre d'exécutions des fonctions scalaires. Il s'agit d'un principe établi de longue date. C'est la «marge de manœuvre» fondamentale qui laisse à l'optimiseur suffisamment de liberté pour obtenir des améliorations significatives dans l'exécution du plan de requête.

2) Ce "comportement une fois par ligne" n'est pas un nouveau problème, bien qu'il ne soit pas largement discuté. Nous avons commencé à modifier son comportement dans la version du Yukon. Mais il est assez difficile de cerner précisément, dans tous les cas, exactement ce que cela signifie! Par exemple, cela s'applique-t-il aux lignes intermédiaires calculées «en route» vers le résultat final? - auquel cas cela dépend clairement du plan choisi. Ou s'applique-t-il uniquement aux lignes qui apparaîtront éventuellement dans le résultat final? - il y a une récursion désagréable qui se passe ici, comme je suis sûr que vous serez d'accord!

3) Comme je l'ai mentionné précédemment, nous optons par défaut pour "optimiser les performances" - ce qui est bon pour 99% des cas. Les 1% des cas où cela pourrait changer les résultats sont assez faciles à repérer - des «fonctions» à effets secondaires comme NEWID - et faciles à «corriger» (la perf de trading, en conséquence). Cette valeur par défaut pour «optimiser à nouveau les performances» est établie de longue date et acceptée. (Oui, ce n'est pas la position choisie par les compilateurs pour les langages de programmation conventionnels, mais qu'il en soit ainsi).

Nos recommandations sont donc les suivantes:

a) Évitez de vous fier à un calendrier non garanti et à une sémantique de nombre d'exécutions. b) Évitez d'utiliser NEWID () au plus profond des expressions de table. c) Utilisez OPTION pour forcer un comportement particulier (trading perf)

J'espère que cette explication aide à clarifier nos raisons pour fermer ce bogue car "ne résoudra pas".


Fait intéressant, AND NOT (s_guid = NEWID())donne le même plan d'exécution

Ceci est une conséquence de la normalisation, qui se produit très tôt lors de la compilation des requêtes. Les deux expressions se compilent exactement sous la même forme normalisée, de sorte que le même plan d'exécution est produit.


Dans ce cas, si nous voulons forcer un plan particulier qui semble éviter le problème, nous pouvons utiliser WITH (FORCESCAN). Pour être certain, nous devons utiliser une variable pour stocker le résultat de NEWID () avant d'exécuter la requête.
Razvan Socol

11

Ceci est documenté (en quelque sorte) ici:

Le nombre d'exécutions réelles d'une fonction spécifiée dans une requête peut varier entre les plans d'exécution créés par l'optimiseur. Un exemple est une fonction invoquée par une sous-requête dans une clause WHERE. Le nombre d'exécutions de la sous-requête et de sa fonction peut varier selon les différents chemins d'accès choisis par l'optimiseur.

Fonctions définies par l'utilisateur

Ce n'est pas le seul formulaire de requête dans lequel le plan de requête exécutera NEWID () plusieurs fois et modifiera le résultat. Ceci est déroutant, mais est en fait critique pour que NEWID () soit utile pour la génération de clés et le tri aléatoire.

Le plus déroutant est que toutes les fonctions non déterministes ne se comportent pas vraiment comme ça. Par exemple, RAND () et GETDATE () ne s'exécuteront qu'une seule fois par requête.


Existe-t-il un article de blog ou similaire qui explique pourquoi / quand le moteur convertira "pas égal" en une plage?
Monsieur Magoo du

3
Pas que je sache de. Peut être routinier car =, <et >peut être évalué efficacement par rapport à un BTree.
David Browne - Microsoft

5

Pour ce qu'il vaut, si vous regardez cet ancien document standard SQL 92 , les exigences concernant l'inégalité sont décrites dans la section " 8.2 <comparison predicate>" comme suit:

1) Soit X et Y deux <élément constructeur de valeur de ligne> correspondant. Soit XV et YV les valeurs représentées respectivement par X et Y.

[...]

ii) "X <> Y" est vrai si et seulement si XV et YV ne sont pas égaux.

[...]

7) Soit Rx et Ry les deux <constructeur de valeur de ligne> du <prédicat de comparaison> et soit RXi et RYi le i-ème <élément constructeur de valeur de ligne> s de Rx et Ry, respectivement. "Rx <comp op> Ry" est vrai, faux ou inconnu comme suit:

[...]

b) "x <> Ry" est vrai si et seulement si RXi <> RYi pour certains i.

[...]

h) "x <> Ry" est faux si et seulement si "Rx = Ry" est vrai.

Remarque: J'ai inclus 7b et 7h pour être complet car ils parlent de <>comparaison - je ne pense pas que la comparaison des constructeurs de valeurs de ligne avec plusieurs valeurs soit implémentée dans T-SQL, à moins que je ne comprenne simplement massivement ce que cela dit - ce qui est tout à fait possible

Ceci est un tas d'ordures déroutantes. Mais si vous voulez continuer à plonger dans des bennes à ordures ...

Je pense que 1.ii est l'élément qui s'applique dans ce scénario, car nous comparons les valeurs des "éléments de constructeur de valeur de ligne".

ii) "X <> Y" est vrai si et seulement si XV et YV ne sont pas égaux.

Fondamentalement, il dit que X <> Yc'est vrai si les valeurs représentées par X et Y ne sont pas égales. Puisqu'il X < Y OR X > Ys'agit d'une réécriture logiquement équivalente de ce prédicat, il est tout à fait cool pour l'optimiseur de l'utiliser.

La norme n'impose aucune contrainte à cette définition liée à la déterminisme (ou autre chose, vous l'obtenez) des éléments du constructeur de la valeur de ligne de chaque côté de l' <>opérateur de comparaison. C'est la responsabilité du code utilisateur de gérer le fait qu'une expression de valeur d'un côté peut être non déterministe.


1
Je m'abstiendrai de voter (en haut ou en bas) mais je ne suis pas convaincu. Les citations que vous fournissez mentionnent la "valeur" . Je crois comprendre que la comparaison se fait entre deux valeurs, une de chaque côté. Pas entre deux (ou plus) instanciations d'une valeur de chaque côté. De plus, la norme (au moins les 92 que vous citez) ne mentionne pas du tout les fonctions non déterministes. Par un raisonnement similaire au vôtre, nous pouvons supposer qu'un produit SQL conforme à la norme ne fournit aucune fonction non déterministe mais uniquement celles mentionnées dans la norme.
ypercubeᵀᴹ

@yper merci pour la rétroaction! Je pense que votre interprétation est certainement valable. C'est la première fois que je lis ce document. Il mentionne des valeurs dans le contexte de la valeur représentée par un "constructeur de valeur de ligne", qui ailleurs dans le document, dit-il, peut être une sous-requête scalaire (parmi beaucoup d'autres choses). La sous-requête scalaire en particulier semble pouvoir être non déterministe. Mais je ne sais vraiment pas de quoi je parle =)
Josh Darnell
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.