Der CASE
Ausdruck ist eines meiner Lieblingskonstrukte in T-SQL. Es ist ziemlich flexibel und manchmal die einzige Möglichkeit, die Reihenfolge zu steuern, in der SQL Server Prädikate auswertet.
Allerdings wird es oft missverstanden.
Was ist der T-SQL-CASE-Ausdruck?
In T-SQL CASE
ist ein Ausdruck, der einen oder mehrere mögliche Ausdrücke auswertet und den ersten geeigneten Ausdruck zurückgibt. Der Begriff Ausdruck mag hier etwas überladen sein, aber im Grunde ist es alles, was als einzelner, skalarer Wert ausgewertet werden kann, z. B. eine Variable, eine Spalte, ein Zeichenfolgenliteral oder sogar die Ausgabe einer integrierten oder skalaren Funktion .
Es gibt zwei Formen von CASE in T-SQL:
- Einfacher CASE-Ausdruck – wenn Sie nur die Gleichheit auswerten müssen:
CASE <input> WHEN <eval> THEN <return> … [ELSE <return>] END
- Gesuchter CASE-Ausdruck – wenn Sie komplexere Ausdrücke wie Ungleichheit, LIKE oder IS NOT NULL auswerten müssen:
CASE WHEN <input_bool> THEN <return> … [ELSE <return>] END
Der Rückgabeausdruck ist immer ein einzelner Wert, und der Ausgabedatentyp wird durch die Datentyppriorität bestimmt.
Wie gesagt, der CASE-Ausdruck wird oft missverstanden; Hier sind einige Beispiele:
CASE ist ein Ausdruck, keine Anweisung
Für die meisten Leute wahrscheinlich nicht wichtig, und vielleicht ist das nur meine pedantische Seite, aber viele Leute nennen es einen CASE
Erklärung – einschließlich Microsoft, dessen Dokumentation statement verwendet und Ausdruck mal austauschbar. Ich finde das etwas nervig (wie row/record und Spalte/Feld ) und obwohl es sich hauptsächlich um Semantik handelt, gibt es einen wichtigen Unterschied zwischen einem Ausdruck und einer Anweisung:Ein Ausdruck gibt ein Ergebnis zurück. Wenn Leute an CASE
denken als Erklärung , führt dies zu Experimenten bei der Codeverkürzung wie folgt:
SELECT CASE [status] WHEN 'A' THEN StatusLabel = 'Authorized', LastEvent = AuthorizedTime WHEN 'C' THEN StatusLabel = 'Completed', LastEvent = CompletedTime END FROM dbo.some_table;
Oder dies:
SELECT CASE WHEN @foo = 1 THEN (SELECT foo, bar FROM dbo.fizzbuzz) ELSE (SELECT blat, mort FROM dbo.splunge) END;
Diese Art von Ablaufsteuerungslogik kann mit CASE
möglich sein Aussagen in anderen Sprachen (wie VBScript), aber nicht in CASE
von Transact-SQL Ausdruck . Um CASE
zu verwenden Innerhalb derselben Abfragelogik müssten Sie einen CASE
verwenden Ausdruck für jede Ausgabespalte:
SELECT StatusLabel = CASE [status] WHEN 'A' THEN 'Authorized' WHEN 'C' THEN 'Completed' END, LastEvent = CASE [status] WHEN 'A' THEN AuthorizedTime WHEN 'C' THEN CompletedTime END FROM dbo.some_table;
CASE schließt nicht immer kurz
Die offizielle Dokumentation implizierte einmal, dass der gesamte Ausdruck kurzgeschlossen wird, was bedeutet, dass der Ausdruck von links nach rechts ausgewertet wird und die Auswertung beendet wird, wenn er auf eine Übereinstimmung trifft:
Die CASE-Anweisung [sic!] wertet ihre Bedingungen sequentiell aus und stoppt bei der ersten Bedingung, deren Bedingung erfüllt ist.Dies ist jedoch nicht immer wahr. Und zu seiner Ehre hat die Seite in einer aktuelleren Version versucht, ein Szenario zu erklären, in dem dies nicht garantiert ist. Aber es wird nur ein Teil der Geschichte:
In einigen Situationen wird ein Ausdruck ausgewertet, bevor eine CASE-Anweisung [sic!] die Ergebnisse des Ausdrucks als Eingabe erhält. Fehler bei der Auswertung dieser Ausdrücke sind möglich. Aggregierte Ausdrücke, die in WHEN-Argumenten einer CASE-Anweisung [sic!] erscheinen, werden zuerst ausgewertet und dann der CASE-Anweisung [sic!] bereitgestellt. Beispielsweise erzeugt die folgende Abfrage einen Fehler bei der Division durch Null, wenn der Wert des MAX-Aggregats erzeugt wird. Dies geschieht vor der Auswertung des CASE-Ausdrucks.Das Beispiel zum Teilen durch Null ist ziemlich einfach zu reproduzieren, und ich habe es in dieser Antwort auf dba.stackexchange.com demonstriert:
DECLARE @i INT = 1; SELECT CASE WHEN @i = 1 THEN 1 ELSE MIN(1/0) END;
Ergebnis:
Msg 8134, Level 16, State 1Dividieren durch Null Fehler aufgetreten.
Es gibt triviale Problemumgehungen (wie ELSE (SELECT MIN(1/0)) END
). ), aber das ist eine echte Überraschung für viele, die die obigen Sätze aus Books Online nicht auswendig gelernt haben. Auf dieses spezielle Szenario wurde ich erstmals in einem Gespräch über einen privaten E-Mail-Verteiler von Itzik Ben-Gan (@ItzikBenGan) aufmerksam, der wiederum zunächst von Jaime Lafargue benachrichtigt wurde. Ich habe den Fehler in Connect #690017 gemeldet:CASE / COALESCE wird nicht immer in Textreihenfolge ausgewertet; es wurde schnell als "By Design" geschlossen. Paul White (Blog | @SQL_Kiwi) reichte daraufhin Connect #691535 ein:Aggregates Don't Follow the Semantics Of CASE, und es wurde als "Fixed" geschlossen. Die Lösung war in diesem Fall die Klarstellung im Artikel Online-Dokumentation; nämlich das Snippet, das ich oben kopiert habe.
Dieses Verhalten kann sich auch in einigen anderen, weniger offensichtlichen Szenarien ergeben. Beispiel:Connect #780132 :FREETEXT() beachtet die Auswertungsreihenfolge in CASE-Anweisungen nicht (keine Aggregate beteiligt) zeigt, dass, nun ja, CASE
Auch bei bestimmten Volltextfunktionen ist die Auswertungsreihenfolge nicht garantiert von links nach rechts. Zu diesem Punkt kommentierte Paul White, dass er auch etwas Ähnliches bei der Verwendung des neuen LAG()
beobachtet habe Funktion, die in SQL Server 2012 eingeführt wurde. Ich habe kein Repro zur Hand, aber ich glaube ihm, und ich glaube nicht, dass wir alle Grenzfälle ausgegraben haben, in denen dies auftreten kann.
Wenn also Aggregate oder nicht-native Dienste wie die Volltextsuche involviert sind, machen Sie bitte keine Annahmen über Kurzschlüsse in einem CASE
Ausdruck.
RAND() kann mehr als einmal ausgewertet werden
Ich sehe oft Leute, die ein einfaches schreiben CASE
Ausdruck, etwa so:
SELECT CASE @variable WHEN 1 THEN 'foo' WHEN 2 THEN 'bar' END
Es ist wichtig zu verstehen, dass dies als Suche ausgeführt wird CASE
Ausdruck, etwa so:
SELECT CASE WHEN @variable = 1 THEN 'foo' WHEN @variable = 2 THEN 'bar' END
Es ist wichtig zu verstehen, dass der ausgewertete Ausdruck mehrmals ausgewertet wird, weil er tatsächlich ausgewertet werden kann mehrmals. Wenn es sich um eine Variable, eine Konstante oder eine Spaltenreferenz handelt, ist dies wahrscheinlich kein echtes Problem; Dinge können sich jedoch schnell ändern, wenn es sich um eine nicht deterministische Funktion handelt. Beachten Sie, dass dieser Ausdruck ein SMALLINT
ergibt zwischen 1 und 3; Wenn Sie es viele Male ausführen, erhalten Sie immer einen dieser drei Werte:
SELECT CONVERT(SMALLINT, 1+RAND()*3);
Setzen Sie dies nun in einen einfachen CASE
-Ausdruck und führen Sie ihn ein Dutzend Mal aus – schließlich erhalten Sie als Ergebnis NULL
:
SELECT [result] = CASE CONVERT(SMALLINT, 1+RAND()*3) WHEN 1 THEN 'one' WHEN 2 THEN 'two' WHEN 3 THEN 'three' END;
Wie kommt es dazu? Nun, der gesamte CASE
expression wird wie folgt zu einem gesuchten Ausdruck erweitert:
SELECT [result] = CASE WHEN CONVERT(SMALLINT, 1+RAND()*3) = 1 THEN 'one' WHEN CONVERT(SMALLINT, 1+RAND()*3) = 2 THEN 'two' WHEN CONVERT(SMALLINT, 1+RAND()*3) = 3 THEN 'three' ELSE NULL -- this is always implicitly there END;
Was wiederum passiert ist, dass jedes WHEN
-Klausel wertet aus und ruft RAND()
auf unabhängig – und in jedem Fall einen anderen Wert ergeben könnte. Nehmen wir an, wir geben den Ausdruck ein und prüfen das erste WHEN
Klausel, und das Ergebnis ist 3; Wir überspringen diese Klausel und fahren fort. Es ist denkbar, dass die nächsten beiden Klauseln beide 1 zurückgeben, wenn RAND()
erneut ausgewertet – in diesem Fall wird keine der Bedingungen als wahr ausgewertet, also das ELSE
übernimmt.
Andere Ausdrücke können mehr als einmal ausgewertet werden
Dieses Problem ist nicht auf RAND()
beschränkt Funktion. Stellen Sie sich den gleichen Stil von Nichtdeterminismus vor, der von diesen beweglichen Zielen ausgeht:
SELECT [crypt_gen] = 1+ABS(CRYPT_GEN_RANDOM(10) % 20), [newid] = LEFT(NEWID(),2), [checksum] = ABS(CHECKSUM(NEWID())%3);
Diese Ausdrücke können bei mehrfacher Auswertung natürlich einen anderen Wert ergeben. Und mit einem gesuchten CASE
-Ausdruck, wird es Zeiten geben, in denen jede Neubewertung zufällig aus der Suche herausfällt, die für den aktuellen WHEN
spezifisch ist , und drücken Sie schließlich ELSE
Klausel. Um sich davor zu schützen, besteht eine Möglichkeit darin, immer Ihren eigenen expliziten ELSE
fest zu codieren; Seien Sie nur vorsichtig mit dem Fallback-Wert, den Sie zurückgeben möchten, da dies einen gewissen Schiefe-Effekt haben wird, wenn Sie nach einer gleichmäßigen Verteilung suchen. Eine andere Möglichkeit besteht darin, einfach das letzte WHEN
zu ändern -Klausel zu ELSE
, aber dies wird immer noch zu einer ungleichmäßigen Verteilung führen. Meiner Meinung nach besteht die bevorzugte Option darin, zu versuchen, SQL Server dazu zu zwingen, die Bedingung einmal auszuwerten (obwohl dies nicht immer innerhalb einer einzelnen Abfrage möglich ist). Vergleichen Sie beispielsweise diese beiden Ergebnisse:
-- Query A: expression referenced directly in CASE; no ELSE: SELECT x, COUNT(*) FROM ( SELECT x = CASE ABS(CHECKSUM(NEWID())%3) WHEN 0 THEN '0' WHEN 1 THEN '1' WHEN 2 THEN '2' END FROM sys.all_columns ) AS y GROUP BY x; -- Query B: additional ELSE clause: SELECT x, COUNT(*) FROM ( SELECT x = CASE ABS(CHECKSUM(NEWID())%3) WHEN 0 THEN '0' WHEN 1 THEN '1' WHEN 2 THEN '2' ELSE '2' END FROM sys.all_columns ) AS y GROUP BY x; -- Query C: Final WHEN converted to ELSE: SELECT x, COUNT(*) FROM ( SELECT x = CASE ABS(CHECKSUM(NEWID())%3) WHEN 0 THEN '0' WHEN 1 THEN '1' ELSE '2' END FROM sys.all_columns ) AS y GROUP BY x; -- Query D: Push evaluation of NEWID() to subquery: SELECT x, COUNT(*) FROM ( SELECT x = CASE x WHEN 0 THEN '0' WHEN 1 THEN '1' WHEN 2 THEN '2' END FROM ( SELECT x = ABS(CHECKSUM(NEWID())%3) FROM sys.all_columns ) AS x ) AS y GROUP BY x;
Verbreitung:
Wert | Abfrage A | Abfrage B | Abfrage C | Abfrage D |
---|---|---|---|---|
NULL | 2.572 | – | – | – |
0 | 2.923 | 2.900 | 2.928 | 2.949 |
1 | 1.946 | 1.959 | 1.927 | 2.896 |
2 | 1.295 | 3.877 | 3.881 | 2.891 |
Verteilung von Werten bei unterschiedlichen Abfragetechniken
In diesem Fall verlasse ich mich darauf, dass SQL Server den Ausdruck in der Unterabfrage auswertet und ihn nicht in den gesuchten CASE
einführt Ausdruck, aber dies soll lediglich zeigen, dass eine gleichmäßigere Verteilung erzwungen werden kann. In Wirklichkeit ist dies möglicherweise nicht immer die Wahl, die der Optimierer trifft, also lernen Sie bitte nicht aus diesem kleinen Trick. :-)
CHOOSE() ist ebenfalls betroffen
Sie werden dies beobachten, wenn Sie CHECKSUM(NEWID())
ersetzen Ausdruck mit dem RAND()
Ausdruck erhalten Sie ganz andere Ergebnisse; vor allem letzteres gibt immer nur einen Wert zurück. Das liegt daran, dass RAND()
, wie GETDATE()
und einige andere eingebaute Funktionen, wird als Laufzeitkonstante besonders behandelt und nur einmal pro Referenz ausgewertet für die ganze Reihe. Beachten Sie, dass es immer noch NULL
zurückgeben kann genau wie die erste Abfrage im vorherigen Codebeispiel.
Auch dieses Problem ist nicht auf den CASE
beschränkt Ausdruck; Sie können ein ähnliches Verhalten bei anderen integrierten Funktionen beobachten, die dieselbe zugrunde liegende Semantik verwenden. Beispiel:CHOOSE
ist lediglich syntaktischer Zucker für einen aufwändiger gesuchten CASE
Ausdruck, und dies ergibt auch NULL
gelegentlich:
SELECT [choose] = CHOOSE(CONVERT(SMALLINT, 1+RAND()*3),'one','two','three');
IIF()
ist eine Funktion, von der ich erwartet hatte, dass sie in dieselbe Falle tappt, aber diese Funktion ist wirklich nur ein gesuchter CASE
Ausdruck mit nur zwei möglichen Ergebnissen und ohne ELSE
– daher ist es schwierig, sich ohne Verschachtelung und Einführung anderer Funktionen ein Szenario vorzustellen, in dem dies unerwartet brechen kann. Während es im einfachen Fall eine anständige Abkürzung für CASE
ist , ist es auch schwierig, irgendetwas Nützliches damit zu tun, wenn Sie mehr als zwei mögliche Ergebnisse benötigen. :-)
COALESCE() ist ebenfalls betroffen
Schließlich sollten wir dieses COALESCE
untersuchen kann ähnliche Probleme haben. Nehmen wir an, dass diese Ausdrücke äquivalent sind:
SELECT COALESCE(@variable, 'constant'); SELECT CASE WHEN @variable IS NOT NULL THEN @variable ELSE 'constant' END);
In diesem Fall @variable
zweimal ausgewertet (wie jede Funktion oder Unterabfrage, wie in diesem Connect-Element beschrieben).
Ich konnte wirklich einige verwirrte Blicke ernten, als ich das folgende Beispiel in einer kürzlichen Forumsdiskussion vorbrachte. Nehmen wir an, ich möchte eine Tabelle mit einer Verteilung von Werten von 1-5 füllen, aber immer wenn eine 3 auftritt, möchte ich stattdessen -1 verwenden. Kein sehr realistisches Szenario, aber einfach zu konstruieren und zu befolgen. Eine Möglichkeit, diesen Ausdruck zu schreiben, ist:
SELECT COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1);
(Auf Englisch, von innen nach außen arbeitend:konvertieren Sie das Ergebnis des Ausdrucks 1+RAND()*5
zu einem smallint; Wenn das Ergebnis dieser Konvertierung 3 ist, setzen Sie es auf NULL
; wenn das Ergebnis davon NULL
ist , setzen Sie es auf -1. Sie könnten dies mit einem ausführlicheren CASE
schreiben Ausdruck, aber prägnant scheint König zu sein.)
Wenn Sie das ein paar Mal ausführen, sollten Sie einen Wertebereich von 1-5 sowie -1 sehen. Sie werden einige Instanzen von 3 sehen, und Sie haben vielleicht auch bemerkt, dass Sie gelegentlich NULL
sehen , obwohl Sie möglicherweise keines dieser Ergebnisse erwarten. Lassen Sie uns die Verteilung überprüfen:
USE tempdb; GO CREATE TABLE dbo.dist(TheNumber SMALLINT); GO INSERT dbo.dist(TheNumber) SELECT COALESCE(NULLIF(CONVERT(SMALLINT,1+RAND()*5),3),-1); GO 10000 SELECT TheNumber, occurences = COUNT(*) FROM dbo.dist GROUP BY TheNumber ORDER BY TheNumber; GO DROP TABLE dbo.dist;
Ergebnisse (Ihre Ergebnisse werden sicherlich variieren, aber der grundlegende Trend sollte ähnlich sein):
DieNummer | Vorkommen |
---|---|
NULL | 1.654 |
-1 | 2.002 |
1 | 1.290 |
2 | 1.266 |
3 | 1.287 |
4 | 1.251 |
5 | 1.250 |
Verteilung von TheNumber mit COALESCE
Aufschlüsselung eines gesuchten CASE-Ausdrucks
Kratzen Sie sich schon am Kopf? Wie machen die Werte NULL
und 3 auftauchen, und warum ist die Verteilung für NULL
und -1 wesentlich höher? Nun, ich werde Ersteres direkt beantworten und Hypothesen für Letzteres einladen.
Der Ausdruck erweitert sich grob logisch wie folgt seit RAND()
innerhalb von NULLIF
zweimal ausgewertet , und multiplizieren Sie das dann mit zwei Auswertungen für jeden Zweig des COALESCE
Funktion. Ich habe keinen Debugger zur Hand, also ist dies nicht unbedingt *genau* das, was in SQL Server gemacht wird, aber es sollte äquivalent genug sein, um den Punkt zu erklären:
SELECT CASE WHEN CASE WHEN CONVERT(SMALLINT,1+RAND()*5) = 3 THEN NULL ELSE CONVERT(SMALLINT,1+RAND()*5) END IS NOT NULL THEN CASE WHEN CONVERT(SMALLINT,1+RAND()*5) = 3 THEN NULL ELSE CONVERT(SMALLINT,1+RAND()*5) END ELSE -1 END END
Sie können also sehen, dass eine mehrfache Bewertung schnell zu einem Choose Your Own Adventure™-Buch werden kann, und wie beide NULL
und 3 sind mögliche Ergebnisse, die bei Prüfung der ursprünglichen Aussage nicht möglich erscheinen. Eine interessante Randnotiz:Dies passiert nicht ganz gleich, wenn Sie das obige Verteilungsskript nehmen und COALESCE
ersetzen mit ISNULL
. In diesem Fall gibt es keine Möglichkeit für einen NULL
Ausgang; die Verteilung ist ungefähr wie folgt:
DieNummer | Vorkommen |
---|---|
-1 | 1.966 |
1 | 1.585 |
2 | 1.644 |
3 | 1.573 |
4 | 1.598 |
5 | 1.634 |
Verteilung von TheNumber mit ISNULL
Auch hier werden Ihre tatsächlichen Ergebnisse sicherlich variieren, sollten aber nicht viel sein. Der Punkt ist, dass wir immer noch sehen können, dass 3 ziemlich oft durch das Raster fällt, aber ISNULL
eliminiert auf magische Weise das Potential für NULL
um es ganz durchzumachen.
Ich habe über einige der anderen Unterschiede zwischen COALESCE
gesprochen und ISNULL
in einem Tipp mit dem Titel „Entscheidung zwischen COALESCE und ISNULL in SQL Server“. Als ich das schrieb, war ich stark dafür, COALESCE
zu verwenden außer in dem Fall, in dem das erste Argument eine Unterabfrage war (wieder aufgrund dieses Fehlers „Funktionslücke“). Jetzt bin ich mir nicht sicher, ob ich das so stark empfinde.
Einfache CASE-Ausdrücke können über Verbindungsserver verschachtelt werden
Eine der wenigen Einschränkungen des CASE
Ausdruck ist, dass er auf 10 Verschachtelungsebenen beschränkt ist. In diesem Beispiel auf dba.stackexchange.com demonstriert Paul White (unter Verwendung von Plan Explorer), dass ein einfacher Ausdruck wie dieser:
SELECT CASE column_name WHEN '1' THEN 'a' WHEN '2' THEN 'b' WHEN '3' THEN 'c' ... END FROM ...
Wird vom Parser auf die gesuchte Form erweitert:
SELECT CASE WHEN column_name = '1' THEN 'a' WHEN column_name = '2' THEN 'b' WHEN column_name = '3' THEN 'c' ... END FROM ...
Kann aber tatsächlich über eine Linked-Server-Verbindung als folgende, viel ausführlichere Abfrage übertragen werden:
SELECT CASE WHEN column_name = '1' THEN 'a' ELSE CASE WHEN column_name = '2' THEN 'b' ELSE CASE WHEN column_name = '3' THEN 'c' ELSE ... ELSE NULL END END END FROM ...
In dieser Situation, obwohl die ursprüngliche Abfrage nur einen einzigen CASE
hatte Ausdruck mit mehr als 10 möglichen Ergebnissen, wenn er an den Verbindungsserver gesendet wurde, waren mehr als 10 verschachtelt CASE
Ausdrücke. Daher wurde, wie zu erwarten, ein Fehler zurückgegeben:
Anweisung(en) konnten nicht vorbereitet werden.
Msg 125, Level 15, State 4
Case-Ausdrücke dürfen nur auf Level 10 verschachtelt werden.
In einigen Fällen können Sie es wie von Paul vorgeschlagen umschreiben, mit einem Ausdruck wie diesem (unter der Annahme von column_name
ist eine varchar-Spalte):
SELECT CASE CONVERT(VARCHAR(MAX), SUBSTRING(column_name, 1, 255)) WHEN 'a' THEN '1' WHEN 'b' THEN '2' WHEN 'c' THEN '3' ... END FROM ...
In manchen Fällen nur der SUBSTRING
kann erforderlich sein, um den Ort zu ändern, an dem der Ausdruck ausgewertet wird; in anderen nur das CONVERT
. Ich habe keine ausführlichen Tests durchgeführt, aber dies hat möglicherweise mit dem Verbindungsserveranbieter, Optionen wie Collation Compatible und Use Remote Collation und der Version von SQL Server an beiden Enden der Pipe zu tun.
Um es kurz zu machen, es ist wichtig, sich daran zu erinnern, dass Ihr CASE
Ausdruck ohne Vorwarnung für Sie neu geschrieben werden kann und dass jede von Ihnen verwendete Problemumgehung später vom Optimierer überschrieben werden kann, selbst wenn sie jetzt für Sie funktioniert.
Abschließende Gedanken zum CASE-Ausdruck und zusätzliche Ressourcen
Ich hoffe, ich habe einige Denkanstöße zu einigen der weniger bekannten Aspekte des CASE
gegeben Ausdruck und einige Einblicke in Situationen, in denen CASE
– und einige der Funktionen, die dieselbe zugrunde liegende Logik verwenden – unerwartete Ergebnisse zurückgeben. Einige andere interessante Szenarien, in denen diese Art von Problem aufgetreten ist:
- Stapelüberlauf:Wie erreicht dieser CASE-Ausdruck die ELSE-Klausel?
- Stapelüberlauf:CRYPT_GEN_RANDOM() Seltsame Effekte
- Stapelüberlauf:CHOOSE() funktioniert nicht wie beabsichtigt
- Stapelüberlauf:CHECKSUM(NewId()) wird mehrmals pro Zeile ausgeführt
- Connect #350485 :Fehler mit NEWID() und Tabellenausdrücken