Database
 sql >> Datenbank >  >> RDS >> Database

Schmutzige Geheimnisse des CASE-Ausdrucks

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 1
Dividieren 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:

Msg 8180, Level 16, State 1
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