Liest SQL Server die gesamte COALESCE-Funktion, auch wenn das erste Argument nicht NULL ist?

98

Ich verwende eine T-SQL- COALESCEFunktion, bei der das erste Argument in etwa 95% der Fälle , in denen es ausgeführt wird, nicht null ist. Wenn das erste Argument ist NULL, ist das zweite Argument ein ziemlich langwieriger Prozess:

SELECT COALESCE(c.FirstName
                ,(SELECT TOP 1 b.FirstName
                  FROM TableA a 
                  JOIN TableB b ON .....)
                )

Wenn zum Beispiel c.FirstName = 'John'würde SQL Server noch die Unterabfrage ausführen?

Ich weiß mit der VB.NET- IIF()Funktion, wenn das zweite Argument True ist, liest der Code immer noch das dritte Argument (obwohl es nicht verwendet wird).

Curt
quelle

Antworten:

95

Nein . Hier ist ein einfacher Test:

SELECT COALESCE(1, (SELECT 1/0)) -- runs fine
SELECT COALESCE(NULL, (SELECT 1/0)) -- throws error

Wenn die zweite Bedingung ausgewertet wird, wird eine Ausnahme für die Division durch Null ausgelöst.

Gemäß der MSDN-Dokumentation hängt dies davon ab, wie COALESCEder Interpreter sie anzeigt - es ist nur eine einfache Möglichkeit, eine CASEErklärung zu verfassen .

CASE ist als eine der wenigen Funktionen in SQL Server bekannt, die (meistens) zuverlässig Kurzschlüsse verursachen.

Es gibt einige Ausnahmen beim Vergleich mit skalaren Variablen und Aggregationen, wie von Aaron Bertrand in einer anderen Antwort hier gezeigt (und dies würde sowohl für CASEals auch gelten COALESCE):

DECLARE @i INT = 1;
SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;

erzeugt eine Division durch Null Fehler.

Dies sollte als Fehler angesehen werden und wird in der Regel COALESCEvon links nach rechts analysiert.

JNK
quelle
6
@JNK sehen Sie sich bitte meine Antwort an, um einen sehr einfachen Fall zu sehen, in dem dies nicht zutrifft (meine Sorge ist, dass es noch mehr, noch unentdeckte Szenarien gibt - was es schwierig macht, sich darauf zu einigen, dass CASEimmer von links nach rechts und immer Kurzschlüsse ausgewertet werden ).
Aaron Bertrand
4
Andere interessante Verhalten @SQLKiwi wies mich auf: SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 1 END), 1);- mehrmals wiederholen. Sie werden NULLmanchmal bekommen . Versuchen Sie es erneut mit ISNULL- Sie werden nie NULL...
Aaron Bertrand
@ Martin ja ich glaube schon. Aber kein Verhalten, das die meisten Benutzer intuitiv finden würden, wenn sie nicht von diesem Problem gehört hätten (oder von ihm gebissen worden wären).
Aaron Bertrand
73

Wie wäre es damit - wie mir Itzik Ben-Gan berichtet hat, dem Jaime Lafargue davon erzählt hat ?

DECLARE @i INT = 1;
SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;

Ergebnis:

Msg 8134, Level 16, State 1, Line 2
Divide by zero error encountered.

Natürlich gibt es triviale Problemumgehungen, aber der Punkt ist immer noch, dass CASEnicht immer eine Auswertung von links nach rechts / ein Kurzschluss gewährleistet ist. Ich habe den Fehler hier gemeldet und er wurde als "beabsichtigt" geschlossen. Paul White hat dieses Connect-Objekt anschließend abgelegt und es wurde als "Fest" geschlossen. Nicht, weil es per se behoben wurde, sondern weil die Onlinedokumentation mit einer genaueren Beschreibung des Szenarios aktualisiert wurde, in dem Aggregate die Auswertungsreihenfolge eines CASEAusdrucks ändern können . Ich habe kürzlich mehr darüber hier gebloggt .

BEARBEITEN Sie nur einen Nachtrag, obwohl ich einverstanden bin, dass dies Randfälle sind, dass Sie sich die meiste Zeit auf die Auswertung von links nach rechts und das Kurzschließen verlassen können und dass dies Fehler sind, die der Dokumentation widersprechen und wahrscheinlich irgendwann behoben werden ( das ist nicht eindeutig - siehe das folgende Gespräch in Bart Duncans Blog-Post, um zu sehen, warum), ich muss nicht zustimmen, wenn Leute sagen, dass etwas immer wahr ist, auch wenn es einen Einzelfall gibt, der es widerlegt. Wenn Itzik und andere einsame Bugs wie dieses finden, ist es zumindest möglich, dass es auch andere Bugs gibt. Und da wir den Rest der OP-Anfrage nicht kennen, können wir nicht mit Sicherheit sagen, dass er sich auf diesen Kurzschluss verlassen wird, aber am Ende von ihm gebissen wird. Für mich ist die sicherere Antwort:

Während Sie sich normalerweise darauf verlassen können CASE, wie in der Dokumentation beschrieben, von links nach rechts und den Kurzschluss zu bewerten, ist es nicht genau zu sagen, dass Sie dies immer tun können. Auf dieser Seite werden zwei Fälle gezeigt, in denen dies nicht zutrifft und keiner der Fehler in einer öffentlich verfügbaren Version von SQL Server behoben wurde.

BEARBEITEN ist ein weiterer Fall (ich muss damit aufhören), in dem ein CASEAusdruck nicht in der erwarteten Reihenfolge ausgewertet wird, obwohl keine Aggregate beteiligt sind.

Aaron Bertrand
quelle
2
Und es sieht so aus, als gäbe es ein anderes Problem CASE , das stillschweigend behoben wurde
Martin Smith
IMO beweist dies nicht, dass die Bewertung des CASE-Ausdrucks nicht garantiert ist, da die aggregierten Werte vor der Auswahl berechnet werden (so dass sie in having verwendet werden können).
Salman A
1
@SalmanA Ich bin mir nicht sicher, was dies möglicherweise außer dem Nachweis genau dieser Reihenfolge der Auswertung in einem CASE-Ausdruck bewirkt, der nicht garantiert ist. Wir bekommen eine Ausnahme, weil das Aggregat zuerst berechnet wird, obwohl dies in einer ELSE-Klausel steht, die - wenn Sie sich die Dokumentation ansehen - niemals erreicht werden sollte.
Aaron Bertrand
@AaronBertrand-Aggregate werden vor der CASE-Anweisung berechnet (und sollten IMO sein). Die überarbeitete Dokumentation weist genau darauf hin, dass der Fehler auftritt, bevor CASE ausgewertet wird.
Salman A
@SalmanA Dem Gelegenheitsentwickler wird immer noch gezeigt, dass der CASE-Ausdruck nicht in der Reihenfolge ausgewertet wird, in der er geschrieben wurde. Die zugrunde liegenden Mechanismen sind irrelevant, wenn Sie nur verstehen möchten, warum ein Fehler von einem CASE-Zweig kommt, der nicht beachtet werden sollte. ' nicht erreicht worden. Haben Sie auch Argumente gegen alle anderen Beispiele auf dieser Seite?
Aaron Bertrand
37

Meine Meinung dazu ist , dass die Dokumentation macht es ziemlich klar , dass die Absicht ist , dass CASE Kurzschluss sollte. Wie Aaron erwähnt, gab es mehrere Fälle (ha!), In denen gezeigt wurde, dass dies nicht immer der Fall ist.

Bisher wurden alle diese Probleme als Fehler erkannt und behoben - obwohl dies nicht unbedingt in einer SQL Server-Version der Fall sein muss, die Sie heute kaufen und patchen können (der ständig klappende Fehler hat es noch nicht zu einem kumulativen Update-AFAIK geschafft). Der neueste potenzielle Fehler, der ursprünglich von Itzik Ben-Gan gemeldet wurde, muss noch untersucht werden (entweder Aaron oder ich werden ihn in Kürze zu Connect hinzufügen).

Bezogen auf die ursprüngliche Frage gibt es andere Probleme mit CASE (und damit COALESCE), bei denen nebenwirkende Funktionen oder Unterabfragen verwendet werden. Erwägen:

SELECT COALESCE((SELECT CASE WHEN RAND() <= 0.5 THEN 999 END), 999);
SELECT ISNULL((SELECT CASE WHEN RAND() <= 0.5 THEN 999 END), 999);

Das COALESCE-Formular gibt häufig NULL zurück. Weitere Informationen finden Sie unter https://connect.microsoft.com/SQLServer/feedback/details/546437/coalesce-subquery-1-may-return-null

Aufgrund der aufgezeigten Probleme mit Optimierungsumwandlungen und der Verfolgung gemeinsamer Ausdrücke kann nicht garantiert werden, dass CASE unter allen Umständen kurzschließt. Ich kann mir Fälle vorstellen, in denen es möglicherweise nicht einmal möglich ist, das Verhalten vorherzusagen, indem ich die Ergebnisse des öffentlichen Showplans inspiziere, obwohl ich heute keinen Repro dafür habe.

Zusammenfassend kann man sagen, dass man ziemlich sicher sein kann, dass CASE im Allgemeinen einen Kurzschluss verursacht (insbesondere dann, wenn eine vernünftige Person den Ausführungsplan überprüft und dieser Ausführungsplan mit einem Planleitfaden oder Hinweisen "durchgesetzt" wird), wenn dies jedoch erforderlich ist Als absolute Garantie müssen Sie SQL schreiben, das den Ausdruck überhaupt nicht enthält.

Ich glaube, das ist kein sehr zufriedenstellender Zustand.

Paul White
quelle
18

Ich habe über einen anderen Fall kommen , wo CASE/ COALESCEnicht Kurzschluss zu tun. Die folgende TVF löst eine PK-Verletzung aus, wenn sie 1als Parameter übergeben wird.

CREATE FUNCTION F (@P INT)
RETURNS @T TABLE (
  C INT PRIMARY KEY)
AS
  BEGIN
      INSERT INTO @T
      VALUES      (1),
                  (@P)

      RETURN
  END

Wenn wie folgt aufgerufen

DECLARE @Number INT = 1

SELECT COALESCE(@Number, (SELECT number
                          FROM   master..spt_values
                          WHERE  type = 'P'
                                 AND number = @Number), 
                         (SELECT TOP (1)  C
                          FROM   F(@Number))) 

Oder als

DECLARE @Number INT = 1

SELECT CASE
         WHEN @Number = 1 THEN @Number
         ELSE (SELECT TOP (1) C
               FROM   F(@Number))
       END 

Beide geben das Ergebnis

Verletzung der PRIMARY KEY-Einschränkung 'PK__F__3BD019A800551192'. Es kann kein doppelter Schlüssel in Objekt 'dbo. @ T' eingefügt werden. Der doppelte Schlüsselwert ist (1).

Dies zeigt, dass die SELECT(oder zumindest die Tabellenvariablenpopulation) immer noch ausgeführt wird und einen Fehler auslöst, obwohl dieser Zweig der Anweisung niemals erreicht werden sollte. Der Plan für die COALESCEVersion ist unten.

Planen

Durch dieses Umschreiben der Abfrage wird das Problem anscheinend vermieden

SELECT COALESCE(Number, (SELECT number
                          FROM   master..spt_values
                          WHERE  type = 'P'
                                 AND number = Number), 
                         (SELECT TOP (1)  C
                          FROM   F(Number))) 
FROM (VALUES(1)) V(Number)   

Welches gibt Plan

Plan2

Martin Smith
quelle
8

Ein anderes Beispiel

CREATE TABLE T1 (C INT PRIMARY KEY)

CREATE TABLE T2 (C INT PRIMARY KEY)

INSERT INTO T1 
OUTPUT inserted.* INTO T2
VALUES (1),(2),(3);

Die Abfrage

SET STATISTICS IO ON;

SELECT T1.C,
       COALESCE(T1.C , CASE WHEN EXISTS (SELECT * FROM T2 WHERE T2.C = T1.C)  THEN -1 END)
FROM T1
OPTION (LOOP JOIN)

Zeigt überhaupt keine Lesevorgänge T2an.

Die Suche nach T2befindet sich unter einem Pass-Through-Prädikat und der Operator wird niemals ausgeführt. Aber

SELECT T1.C,
       COALESCE(T1.C , CASE WHEN EXISTS (SELECT * FROM T2 WHERE T2.C = T1.C)  THEN -1 END)
FROM T1
OPTION (MERGE JOIN)

Zeigt , dass T2gelesen wird. Auch wenn eigentlich nie ein Wert von T2benötigt wird.

Das ist natürlich nicht wirklich überraschend, aber ich dachte, es lohnt sich, es dem Gegenbeispiel-Repository hinzuzufügen, schon allein deshalb, weil es die Frage aufwirft, was Kurzschluss in einer satzbasierten deklarativen Sprache überhaupt bedeutet.

Martin Smith
quelle
7

Ich wollte nur eine Strategie erwähnen, die Sie vielleicht nicht in Betracht gezogen haben. Es mag hier kein Spiel sein, aber manchmal ist es nützlich. Überprüfen Sie, ob diese Änderung zu einer besseren Leistung führt:

SELECT COALESCE(c.FirstName
            ,(SELECT TOP 1 b.FirstName
              FROM TableA a 
              JOIN TableB b ON .....
              WHERE C.FirstName IS NULL) -- this is the changed part
            )

Ein anderer Weg, dies zu tun, könnte folgender sein (im Grunde genommen äquivalent, aber Sie können bei Bedarf über die andere Abfrage auf mehr Spalten zugreifen):

SELECT COALESCE(c.FirstName, x.FirstName)
FROM
   TableC c
   OUTER APPLY (
      SELECT TOP 1 b.FirstName
      FROM
         TableA a 
         JOIN TableB b ON ...
      WHERE
         c.FirstName IS NULL -- the important part
   ) x

Grundsätzlich ist dies eine Technik des "harten" Verbindens von Tabellen, aber einschließlich der Bedingung, wann überhaupt irgendwelche Zeilen verbunden werden sollten. Nach meiner Erfahrung hat dies zuweilen den Ausführungsplänen sehr geholfen.

ErikE
quelle
3

Nein, das würde es nicht. Es würde nur laufen, wenn es c.FirstNameist NULL.

Sie sollten es jedoch selbst versuchen. Experiment. Sie sagten, Ihre Unterabfrage sei langwierig. Benchmark. Ziehen Sie daraus Ihre eigenen Schlüsse.

Die @ Aaron-Antwort auf die ausgeführte Unterabfrage ist vollständiger.

Ich denke jedoch immer noch, dass Sie Ihre Abfrage überarbeiten und verwenden sollten LEFT JOIN. In den meisten Fällen können Unterabfragen entfernt werden, indem Sie Ihre Abfrage für die Verwendung von LEFT JOINs überarbeiten .

Das Problem bei der Verwendung von Unterabfragen besteht darin, dass Ihre Gesamtanweisung langsamer ausgeführt wird, da die Unterabfrage für jede Zeile in der Ergebnismenge der Hauptabfrage ausgeführt wird.

Adrian
quelle
@Adrian es ist immer noch nicht richtig. Wenn Sie sich den Ausführungsplan ansehen, werden Sie feststellen, dass Unterabfragen häufig recht geschickt in JOINs konvertiert werden. Es ist ein reiner Gedankenexperimentfehler anzunehmen, dass die gesamte Unterabfrage für jede Zeile wiederholt werden muss, obwohl dies effektiv passieren kann , wenn eine mit einem Scan verknüpfte verschachtelte Schleife ausgewählt wird.
ErikE
3

Der eigentliche Standard besagt, dass alle WHEN-Klauseln (sowie die ELSE-Klausel) analysiert werden müssen, um den Datentyp des Ausdrucks als Ganzes zu bestimmen. Ich müsste wirklich einige meiner alten Notizen herausholen, um festzustellen, wie ein Fehler behandelt wird. Aber 1/0 verwendet ganzzahlige Werte, also würde ich davon ausgehen, dass dies ein Fehler ist. Es ist ein Fehler mit dem Integer-Datentyp. Wenn Sie nur Nullen in der Zusammenführungsliste haben, ist es etwas schwieriger, den Datentyp zu bestimmen, und das ist ein weiteres Problem.

Joe Celko
quelle