it-swarm.com.de

SQL Server teilt A <> B in A <B OR A> B) auf und liefert seltsame Ergebnisse, wenn B nicht deterministisch ist

Wir haben ein interessantes Problem mit SQL Server festgestellt. Betrachten Sie das folgende Repro-Beispiel:

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;

Geige

Bitte vergessen Sie für einen Moment, dass die Bedingung s_guid <> NEWID() völlig nutzlos erscheint - dies ist nur ein minimales Repro-Beispiel. Da die Wahrscheinlichkeit, dass NEWID() mit einem bestimmten konstanten Wert übereinstimmt, extrem gering ist, sollte sie jedes Mal mit TRUE bewertet werden.

Aber das tut es nicht. Das Ausführen dieser Abfrage gibt normalerweise 1 Zeile zurück, aber manchmal (ziemlich häufig, mehr als 1 von 10) gibt 0 Zeilen zurück. Ich habe es mit SQL Server 2008 auf meinem System reproduziert, und Sie können es online mit der oben verlinkten Geige reproduzieren (SQL Server 2014).

Ein Blick auf den Ausführungsplan zeigt, dass der Abfrageanalysator die Bedingung anscheinend in s_guid < NEWID() OR s_guid > NEWID() aufteilt:

(query plan screenshot

... was vollständig erklärt, warum es manchmal fehlschlägt (wenn die erste generierte ID kleiner und die zweite größer als die angegebene ID ist).

Darf SQL Server A <> B Als A < B OR A > B Auswerten, auch wenn einer der Ausdrücke nicht deterministisch ist? Wenn ja, wo ist es dokumentiert? Oder haben wir einen Fehler gefunden?

Interessanterweise liefert AND NOT (s_guid = NEWID()) denselben Ausführungsplan (und dasselbe zufällige Ergebnis).

Wir haben dieses Problem festgestellt, als ein Entwickler eine bestimmte Zeile optional ausschließen wollte und Folgendes verwendete:

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

als "Abkürzung" für:

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

Ich suche nach Dokumentation und/oder Bestätigung eines Fehlers. Der Code ist nicht allzu relevant, sodass keine Problemumgehungen erforderlich sind.

26
Heinzi

Darf SQL Server A <> B Als A < B OR A > B Auswerten, auch wenn einer der Ausdrücke nicht deterministisch ist?

Dies ist ein etwas kontroverser Punkt, und die Antwort ist ein qualifiziertes "Ja".

Die beste Diskussion, die mir bekannt ist, wurde als Antwort auf den Connect-Fehlerbericht von Itzik Ben-Gan gegeben Fehler mit NEWID und Tabellenausdrücken , der geschlossen wurde, da er nicht behoben werden konnte. Connect wurde inzwischen eingestellt, sodass der Link zu einem Webarchiv besteht. Leider ging durch den Niedergang von Connect viel nützliches Material verloren (oder wurde schwerer zu finden). Die nützlichsten Zitate von Jim Hogg von Microsoft sind jedenfalls:

Dies trifft den Kern des Problems - darf die Optimierung die Semantik eines Programms ändern? Dh wenn ein Programm bestimmte Antworten liefert, aber langsam läuft, ist es legitim, dass ein Abfrageoptimierer dieses Programm schneller laufen lässt, aber auch die angegebenen Ergebnisse ändert?

Bevor Sie "NEIN!" (auch meine persönliche Neigung :-), bedenken Sie: Die gute Nachricht ist, dass in 99% der Fälle die Antworten gleich sind. Die Abfrageoptimierung ist also ein klarer Gewinn. Die schlechte Nachricht ist, dass, wenn die Abfrage nebenwirkenden Code enthält, unterschiedliche Pläne tatsächlich unterschiedliche Ergebnisse liefern können. Und NEWID () ist eine solche nebenwirksame (nicht deterministische) 'Funktion', die den Unterschied aufdeckt. [Wenn Sie experimentieren, können Sie andere entwickeln - zum Beispiel eine Kurzschlussbewertung von AND-Klauseln: Lassen Sie die zweite Klausel eine arithmetische Division durch Null auslösen - verschiedene Optimierungen können diese zweite Klausel VOR der ersten Klausel ausführen] Dies spiegelt sich wider Craig erklärt an anderer Stelle in diesem Thread, dass SqlServer nicht garantiert, wenn Skalaroperatoren ausgeführt werden.

Wir haben also die Wahl: Wenn wir ein bestimmtes Verhalten bei Vorhandensein von nicht deterministischem (nebenwirkendem) Code garantieren möchten - damit beispielsweise die Ergebnisse von JOINs der Semantik einer Ausführung mit verschachtelten Schleifen folgen - dann sind wir kann geeignete OPTIONEN verwenden, um dieses Verhalten zu erzwingen - wie UC betont. Der resultierende Code wird jedoch langsam ausgeführt - das sind die Kosten für das Humpeln des Abfrageoptimierers.

Trotzdem bewegen wir das Abfrageoptimierungsprogramm in Richtung "wie erwartet" für NEWID () - Kompromiss zwischen Leistung und "erwarteten Ergebnissen".

Ein Beispiel für die diesbezügliche Verhaltensänderung im Laufe der Zeit ist NULLIF funktioniert falsch mit nicht deterministischen Funktionen wie Rand () . Es gibt auch andere ähnliche Fälle, in denen z.B. COALESCE mit einer Unterabfrage, die zu unerwarteten Ergebnissen führen kann und die auch schrittweise behandelt wird.

Jim fährt fort:

Den Kreis schließen . . . Ich habe diese Frage mit dem Entwicklerteam besprochen. Und schließlich haben wir uns aus folgenden Gründen entschieden, das aktuelle Verhalten nicht zu ändern:

1) Der Optimierer garantiert nicht das Timing oder die Anzahl der Ausführungen von Skalarfunktionen. Dies ist ein seit langem etablierter Grundsatz. Dies ist der grundlegende „Spielraum“, der dem Optimierer genügend Freiheit lässt, um signifikante Verbesserungen bei der Ausführung von Abfrageplänen zu erzielen.

2) Dieses "Verhalten einmal pro Zeile" ist kein neues Problem, obwohl es nicht allgemein diskutiert wird. Wir haben bereits in der Yukon-Version damit begonnen, sein Verhalten zu optimieren. Aber es ist ziemlich schwierig, in jedem Fall genau zu bestimmen, was es bedeutet! Gilt dies beispielsweise für Zwischenzeilen, die auf dem Weg zum Endergebnis berechnet wurden? - In diesem Fall hängt es eindeutig vom gewählten Plan ab. Oder gilt es nur für die Zeilen, die schließlich im fertigen Ergebnis erscheinen? - Hier findet eine böse Rekursion statt, da werden Sie sicher zustimmen!

3) Wie bereits erwähnt, wird standardmäßig die Leistung optimiert - was in 99% der Fälle gut ist. Die 1% der Fälle, in denen sich die Ergebnisse ändern könnten, sind ziemlich leicht zu erkennen - nebeneffektive 'Funktionen' wie NEWID - und leicht 'zu beheben' (Handelsperf als Konsequenz). Diese Standardeinstellung zum erneuten "Optimieren der Leistung" ist seit langem etabliert und wird akzeptiert. (Ja, es ist nicht die Haltung, die Compiler für herkömmliche Programmiersprachen gewählt haben, aber so sei es).

Unsere Empfehlungen lauten also:

a) Vermeiden Sie es, sich auf nicht garantierte Zeit- und Ausführungssemantik zu verlassen. b) Vermeiden Sie die Verwendung von NEWID () in Tabellenausdrücken. c) Verwenden Sie OPTION, um ein bestimmtes Verhalten zu erzwingen (Trading Perf)

Hoffe, diese Erklärung hilft, unsere Gründe für das Schließen dieses Fehlers zu klären, da "nicht behoben werden kann".


Interessanterweise liefert AND NOT (s_guid = NEWID()) den gleichen Ausführungsplan

Dies ist eine Folge der Normalisierung, die sehr früh während der Abfragekompilierung auftritt. Beide Ausdrücke werden in genau derselben normalisierten Form kompiliert, sodass derselbe Ausführungsplan erstellt wird.

22
Paul White 9

Dies ist hier irgendwie dokumentiert:

Die Häufigkeit, mit der eine in einer Abfrage angegebene Funktion tatsächlich ausgeführt wird, kann zwischen den vom Optimierer erstellten Ausführungsplänen variieren. Ein Beispiel ist eine Funktion, die von einer Unterabfrage in einer WHERE-Klausel aufgerufen wird. Die Häufigkeit, mit der die Unterabfrage und ihre Funktion ausgeführt werden, kann je nach den vom Optimierer ausgewählten Zugriffspfaden variieren.

Benutzerdefinierte Funktionen

Dies ist nicht das einzige Abfrageformular, in dem der Abfrageplan NEWID () mehrmals ausführt und das Ergebnis ändert. Dies ist verwirrend, aber tatsächlich wichtig, damit NEWID () für die Schlüsselgenerierung und zufällige Sortierung nützlich ist.

Am verwirrendsten ist, dass sich nicht alle nicht deterministischen Funktionen tatsächlich so verhalten. Zum Beispiel werden Rand () und GETDATE () nur einmal pro Abfrage ausgeführt.

Wenn Sie sich dieses altes SQL 92-Standarddokument ansehen, werden die Anforderungen an die Ungleichung im Abschnitt "8.2 <comparison predicate>" Wie folgt beschrieben:

1) Sei X und Y zwei beliebige entsprechende <Zeilenwertkonstruktorelemente>. Sei XV und YV die durch X bzw. Y dargestellte Werte.

[...]

ii) "X Y" ist genau dann wahr, wenn XV und YV nicht gleich sind.

[...]

7) Sei Rx und Ry die beiden <Zeilenwertkonstruktoren> des <Vergleichsprädikats> und sei RXi und RYi das i-te <Zeilenwertkonstruktorelement> von Rx bzw. Ry. "Rx <comp op> Ry" ist wie folgt wahr, falsch oder unbekannt:

[...]

b) "x <> Ry" ist genau dann wahr, wenn RXi <> RYi für einige i.

[...]

h) "x <> Ry" ist genau dann falsch, wenn "Rx = Ry" wahr ist.

Hinweis: Der Vollständigkeit halber habe ich 7b und 7h eingefügt, da sie über den Vergleich von <> Sprechen. Ich glaube nicht, dass der Vergleich von Zeilenwertkonstruktoren mit mehreren Werten in T-SQL implementiert ist, es sei denn, ich verstehe das nur massiv falsch sagt - was durchaus möglich ist

Dies ist ein Haufen verwirrender Müll. Aber wenn Sie weiterhin Müllcontainer tauchen wollen ...

I think dass 1.ii das Element ist, das in diesem Szenario gilt, da wir die Werte von "Zeilenwertkonstruktorelementen" vergleichen.

ii) "X <> Y" ist genau dann wahr, wenn XV und YV nicht gleich sind.

Grundsätzlich heißt es, dass X <> Y Wahr ist, wenn die durch X und Y dargestellten Werte Nicht gleich sind. Da X < Y OR X > Y Eine logisch äquivalente Neufassung dieses Prädikats ist, ist es für den Optimierer absolut cool, dies zu verwenden.

Der Standard legt dieser Definition keine Einschränkungen in Bezug auf die Deterministik (oder was auch immer, Sie erhalten es) der Zeilenwertkonstruktorelemente auf beiden Seiten des Vergleichsoperators <> Auf. Es liegt in der Verantwortung des Benutzercodes, sich mit der Tatsache zu befassen, dass ein Werteausdruck auf einer Seite möglicherweise nicht deterministisch ist.

5
Josh Darnell