web-dev-qa-db-fra.com

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

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 condition s_guid <> NEWID() semble totalement inutile - ce n'est qu'un exemple de repro minimal. Étant donné que la probabilité que NEWID() corresponde à 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 habituellement renvoie 1 ligne, mais parfois (assez fréquemment, plus d'une fois sur 10) renvoie 0 lignes. 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():

query plan screenshot

... 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 A <> B Comme 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.

26
Heinzi

SQL Server est-il autorisé à évaluer A <> B Comme 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 que je connaisse a été donnée en réponse au rapport de bogue Connect d'Itzik Ben-Gan Bogue 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 pour un optimiseur de requête de faire en sorte que ce programme s'exécute plus rapidement, 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 ne fonctionne pas correctement avec des fonctions non déterministes telles que Rand () . Il existe également d'autres cas similaires utilisant par exemple COALESCE avec une sous-requête qui peut produire des résultats inattendus et qui est également traitée 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 cours" 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 à "réparer" (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.

22
Paul White 9

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 où le plan de requête exécutera NEWID () plusieurs fois et modifiera le résultat. Ceci est déroutant, mais est en fait essentiel 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 comme ça. Par exemple, Rand () et GETDATE () ne s'exécuteront qu'une seule fois par requête.

Pour ce que ça vaut, si vous regardez ceci 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 tout 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, cela dit X <> Y est vrai si les valeurs représentées par X et Y ne sont pas égales. Depuis X < Y OR X > Y est une réécriture logiquement équivalente de ce prédicat, c'est vraiment cool pour l'optimiseur de l'utiliser.

La norme ne met aucune contrainte sur 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é du <> opérateur de comparaison. Il est de la responsabilité du code utilisateur de gérer le fait qu'une expression de valeur d'un côté peut être non déterministe.

5
Josh Darnell