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

Grundlagen von Tabellenausdrücken, Teil 2 – Abgeleitete Tabellen, logische Überlegungen

Letzten Monat habe ich einen Hintergrund zu Tabellenausdrücken in T-SQL bereitgestellt. Ich habe den Kontext aus der relationalen Theorie und dem SQL-Standard erklärt. Ich habe erklärt, wie eine Tabelle in SQL ein Versuch ist, eine Relation aus der relationalen Theorie darzustellen. Ich habe auch erklärt, dass ein relationaler Ausdruck ein Ausdruck ist, der auf einer oder mehreren Beziehungen als Eingaben operiert und zu einer Beziehung führt. In ähnlicher Weise ist ein Tabellenausdruck in SQL ein Ausdruck, der auf einer oder mehreren Eingabetabellen operiert und zu einer Tabelle führt. Der Ausdruck kann eine Abfrage sein, muss es aber nicht. Der Ausdruck kann beispielsweise ein Tabellenwertkonstruktor sein, wie ich später in diesem Artikel erläutern werde. Ich habe auch erklärt, dass ich mich in dieser Reihe auf vier spezifische Arten benannter Tabellenausdrücke konzentriere, die T-SQL unterstützt:abgeleitete Tabellen, allgemeine Tabellenausdrücke (CTEs), Ansichten und Inline-Tabellenwertfunktionen (TVFs).

Wenn Sie schon länger mit T-SQL arbeiten, sind Sie wahrscheinlich auf einige Fälle gestoßen, in denen Sie entweder Tabellenausdrücke verwenden mussten oder es im Vergleich zu alternativen Lösungen, die diese nicht verwenden, irgendwie bequemer waren. Hier sind nur einige Beispiele für Anwendungsfälle, die Ihnen in den Sinn kommen:

  • Erstellen Sie eine modulare Lösung, indem Sie komplexe Aufgaben in Schritte unterteilen, die jeweils durch einen anderen Tabellenausdruck dargestellt werden.
  • Mischen von Ergebnissen gruppierter Abfragen und Details, falls Sie sich entscheiden, keine Fensterfunktionen für diesen Zweck zu verwenden.
  • Logische Abfrageverarbeitung verarbeitet Abfrageklauseln in der folgenden Reihenfolge:FROM>WHERE>GROUP BY>HAVING>SELECT>ORDER BY. Folglich sind auf derselben Verschachtelungsebene Spaltenaliase, die Sie in der SELECT-Klausel definieren, nur für die ORDER BY-Klausel verfügbar. Sie stehen den übrigen Abfrageklauseln nicht zur Verfügung. Mit Tabellenausdrücken können Sie Aliase, die Sie in einer inneren Abfrage definieren, in jeder Klausel der äußeren Abfrage wiederverwenden und auf diese Weise die Wiederholung langer/komplexer Ausdrücke vermeiden.
  • Fensterfunktionen können nur in den SELECT- und ORDER BY-Klauseln einer Abfrage vorkommen. Bei Tabellenausdrücken können Sie einem Ausdruck basierend auf einer Fensterfunktion einen Alias ​​zuweisen und diesen Alias ​​dann in einer Abfrage für den Tabellenausdruck verwenden.
  • Ein PIVOT-Operator umfasst drei Elemente:Gruppierung, Verbreitung und Aggregation. Dieser Operator identifiziert das Gruppierungselement implizit durch Eliminierung. Mithilfe eines Tabellenausdrucks können Sie genau die drei Elemente projizieren, die beteiligt sein sollen, und die äußere Abfrage den Tabellenausdruck als Eingabetabelle des PIVOT-Operators verwenden lassen, wodurch gesteuert wird, welches Element das Gruppierungselement ist.
  • Modifikationen mit TOP unterstützen keine ORDER BY-Klausel. Sie können indirekt steuern, welche Zeilen ausgewählt werden, indem Sie einen Tabellenausdruck basierend auf einer SELECT-Abfrage mit dem TOP- oder OFFSET-FETCH-Filter und einer ORDER BY-Klausel definieren und die Änderung auf den Tabellenausdruck anwenden.

Dies ist bei weitem keine vollständige Liste. Ich werde einige der oben genannten Anwendungsfälle und andere in dieser Serie demonstrieren. Ich wollte hier nur einige Anwendungsfälle erwähnen, um zu veranschaulichen, wie wichtig Tabellenausdrücke in unserem T-SQL-Code sind und warum es sich lohnt, in das Verständnis ihrer Grundlagen zu investieren.

Im Artikel dieses Monats konzentriere ich mich speziell auf die logische Behandlung abgeleiteter Tabellen.

In meinen Beispielen verwende ich eine Beispieldatenbank namens TSQLV5. Sie finden das Skript, das es erstellt und füllt, hier und sein ER-Diagramm hier.

Abgeleitete Tabellen

Der Begriff abgeleitete Tabelle wird in SQL und T-SQL mit mehr als einer Bedeutung verwendet. Daher möchte ich zunächst klarstellen, auf welche ich mich in diesem Artikel beziehe. Ich beziehe mich auf ein bestimmtes Sprachkonstrukt, das Sie normalerweise, aber nicht nur, in der FROM-Klausel einer äußeren Abfrage definieren. Ich werde die Syntax für dieses Konstrukt in Kürze bereitstellen.

Die allgemeinere Verwendung des Begriffs abgeleitete Tabelle in SQL ist das Gegenstück zu einer abgeleiteten Relation aus der relationalen Theorie. Eine abgeleitete Relation ist eine Ergebnisrelation, die von einer oder mehreren eingegebenen Basisrelationen abgeleitet wird, indem relationale Operatoren aus der relationalen Algebra wie Projektion, Schnittmenge und andere auf diese Basisrelationen angewendet werden. In ähnlicher Weise ist eine abgeleitete Tabelle in SQL im allgemeinen Sinne eine Ergebnistabelle, die von einer oder mehreren Basistabellen abgeleitet wird, indem Ausdrücke anhand dieser Eingabe-Basistabellen ausgewertet werden.

Nebenbei habe ich überprüft, wie der SQL-Standard eine Basistabelle definiert, und es tat mir sofort leid, dass ich mich darum gekümmert habe.

4.15.2 Basistabellen

Eine Basistabelle ist entweder eine persistente Basistabelle oder eine temporäre Tabelle.

Eine persistente Basistabelle ist entweder eine reguläre persistente Basistabelle oder eine vom System versionierte Tabelle.

Eine reguläre Basistabelle ist entweder eine reguläre persistente Basistabelle oder eine temporäre Tabelle.“

Hier hinzugefügt ohne weitere Kommentare…

In T-SQL können Sie eine Basistabelle mit einer CREATE TABLE-Anweisung erstellen, aber es gibt andere Optionen, z. B. SELECT INTO und DECLARE @T AS TABLE.

Hier ist die Standarddefinition für abgeleitete Tabellen im allgemeinen Sinne:

4.15.3 Abgeleitete Tabellen

Eine abgeleitete Tabelle ist eine Tabelle, die direkt oder indirekt von einer oder mehreren anderen Tabellen durch die Auswertung eines Ausdrucks abgeleitet wird, z. B. einer , , oder

. Ein kann eine optionale enthalten. Die Reihenfolge der Zeilen der durch den angegebenen Tabelle wird nur für den garantiert, der unmittelbar die enthält.“

Hier gibt es ein paar interessante Dinge über abgeleitete Tabellen im allgemeinen Sinne zu beachten. Einer hat mit dem Kommentar zur Bestellung zu tun. Dazu komme ich später im Artikel. Ein weiterer Grund ist, dass eine abgeleitete Tabelle in SQL ein gültiger eigenständiger Tabellenausdruck sein kann, aber nicht muss. Der folgende Ausdruck stellt beispielsweise eine abgeleitete Tabelle dar und ist wird auch als gültiger eigenständiger Tabellenausdruck betrachtet (Sie können ihn ausführen):

SELECT custid, companyname
FROM Sales.Customers
WHERE country = N'USA'

Umgekehrt stellt der folgende Ausdruck eine abgeleitete Tabelle dar, ist aber keine ein gültiger eigenständiger Tabellenausdruck:

T1 INNER JOIN T2
  ON T1.keycol = T2.keycol

T-SQL unterstützt eine Reihe von Tabellenoperatoren, die eine abgeleitete Tabelle ergeben, aber nicht als eigenständige Ausdrücke unterstützt werden. Das sind:JOIN, PIVOT, UNPIVOT und APPLY. Sie benötigen eine Klausel, in der sie operieren können (normalerweise FROM, aber auch die USING-Klausel der MERGE-Anweisung) und eine Host-Abfrage.

Von hier an verwende ich den Begriff abgeleitete Tabelle, um ein spezifischeres Sprachkonstrukt zu beschreiben, und nicht im oben beschriebenen allgemeinen Sinne.

Syntax

Eine abgeleitete Tabelle kann als Teil einer äußeren SELECT-Anweisung in ihrer FROM-Klausel definiert werden. Es kann auch als Teil von DELETE- und UPDATE-Anweisungen in ihrer FROM-Klausel und als Teil einer MERGE-Anweisung in ihrer USING-Klausel definiert werden. Ich werde später in diesem Artikel weitere Details zur Syntax bereitstellen, wenn sie in Änderungsanweisungen verwendet wird.

Hier ist die Syntax für eine vereinfachte SELECT-Abfrage für eine abgeleitete Tabelle:

SELECT
FROM ( ) [ AS ] [ () ];

Die abgeleitete Tabellendefinition erscheint dort, wo normalerweise eine Basistabelle erscheinen kann, in der FROM-Klausel der äußeren Abfrage. Es kann eine Eingabe für einen Tabellenoperator wie JOIN, APPLY, PIVOT und UNPIVOT sein. Wenn er als richtige Eingabe für einen APPLY-Operator verwendet wird, darf der Teil

der abgeleiteten Tabelle Korrelationen zu Spalten aus einer äußeren Tabelle haben (mehr dazu in einem speziellen zukünftigen Artikel in der Serie). Andernfalls muss der Tabellenausdruck in sich abgeschlossen sein.

Die äußere Anweisung kann alle üblichen Abfrageelemente enthalten. Im Fall einer SELECT-Anweisung:WHERE, GROUP BY, HAVING, ORDER BY und wie erwähnt Tabellenoperatoren in der FROM-Klausel.

Hier ist ein Beispiel für eine einfache Abfrage einer abgeleiteten Tabelle, die Kunden in den USA darstellt:

SELECT custid, companyname
FROM ( SELECT custid, companyname
       FROM Sales.Customers
       WHERE country = N'USA' ) AS UC;

Diese Abfrage generiert die folgende Ausgabe:

custid  companyname
------- ---------------
32      Customer YSIQX
36      Customer LVJSO
43      Customer UISOJ
45      Customer QXPPT
48      Customer DVFMB
55      Customer KZQZT
65      Customer NYUHS
71      Customer LCOUJ
75      Customer XOJYP
77      Customer LCYBZ
78      Customer NLTYP
82      Customer EYHKM
89      Customer YBQTI

In einer Anweisung, die eine abgeleitete Tabellendefinition beinhaltet, sind drei Hauptteile zu identifizieren:

  1. Der Tabellenausdruck (die innere Abfrage)
  2. Der abgeleitete Tabellenname, oder genauer gesagt, was in der relationalen Theorie als Bereichsvariable betrachtet wird
  3. Die äußere Anweisung

Der Tabellenausdruck soll eine Tabelle darstellen und muss daher bestimmte Anforderungen erfüllen, die eine normale Abfrage nicht unbedingt erfüllen muss. Ich werde die Details in Kürze im Abschnitt „Ein Tabellenausdruck ist eine Tabelle“ erläutern.

Wie für den Namen der abgeleiteten Zieltabelle; Eine gängige Annahme unter T-SQL-Entwicklern ist, dass es sich lediglich um einen Namen oder Alias ​​handelt, den Sie der Zieltabelle zuweisen. Betrachten Sie in ähnlicher Weise die folgende Abfrage:

SELECT custid, companyname
FROM Sales.Customers AS C
WHERE country = N'USA';

Auch hier wird allgemein angenommen, dass AS C nur eine Möglichkeit zum Umbenennen oder Aliasieren der Tabelle Customers für die Zwecke dieser Abfrage ist, beginnend mit dem logischen Abfrageverarbeitungsschritt, in dem der Name zugewiesen wird, und weiter. Aus Sicht der relationalen Theorie hat das, was C darstellt, jedoch eine tiefere Bedeutung. C ist eine sogenannte Bereichsvariable. C ist eine abgeleitete Beziehungsvariable, die sich über die Tupel in der Eingabebeziehungsvariablen Kunden erstreckt. Im obigen Beispiel reicht C über die Tupel in Customers und wertet das Prädikat Land =N'USA' aus. Tupel, für die das Prädikat als wahr ausgewertet wird, werden Teil der Ergebnisrelation C.

Ein Tabellenausdruck ist eine Tabelle

Mit dem Hintergrund, den ich bisher geliefert habe, sollte das, was ich als nächstes erklären werde, wenig überraschend sein. Der Teil einer abgeleiteten Tabellendefinition ist eine Tabelle . Das ist auch dann der Fall, wenn es als Abfrage ausgedrückt wird. Erinnern Sie sich an die Abschlusseigenschaft der relationalen Algebra? Das Gleiche gilt für die übrigen oben genannten Tabellenausdrücke (CTEs, Views und Inline-TVFs). Wie Sie bereits gelernt haben, ist die Tabelle von SQL ist das Gegenstück zur relationalen Theorie relation , wenn auch kein perfektes Gegenstück. Ein Tabellenausdruck muss also bestimmte Anforderungen erfüllen, damit das Ergebnis eine Tabelle ist – Anforderungen, die eine Abfrage, die nicht als Tabellenausdruck verwendet wird, nicht unbedingt erfüllen muss. Hier sind drei spezifische Anforderungen:

  • Alle Spalten des Tabellenausdrucks müssen Namen haben
  • Alle Spaltennamen des Tabellenausdrucks müssen eindeutig sein
  • Die Zeilen des Tabellenausdrucks haben keine Reihenfolge

Lassen Sie uns diese Anforderungen einzeln aufschlüsseln und die Relevanz sowohl für die relationale Theorie als auch für SQL erörtern.

Alle Spalten müssen Namen haben

Denken Sie daran, dass eine Relation eine Überschrift und einen Körper hat. Die Überschrift einer Relation ist eine Menge von Attributen (Spalten in SQL). Ein Attribut hat einen Namen und einen Typnamen und wird durch seinen Namen identifiziert. Eine Abfrage, die nicht als Tabellenausdruck verwendet wird, muss nicht unbedingt allen Zielspalten Namen zuweisen. Betrachten Sie die folgende Abfrage als Beispiel:

SELECT empid, firstname, lastname,
  CONCAT_WS(N'/', country, region, city)
FROM HR.Employees;

Diese Abfrage generiert die folgende Ausgabe:

empid  firstname  lastname   (No column name)
------ ---------- ---------- -----------------
1      Sara       Davis      USA/WA/Seattle
2      Don        Funk       USA/WA/Tacoma
3      Judy       Lew        USA/WA/Kirkland
4      Yael       Peled      USA/WA/Redmond
5      Sven       Mortensen  UK/London
6      Paul       Suurs      UK/London
7      Russell    King       UK/London
8      Maria      Cameron    USA/WA/Seattle
9      Patricia   Doyle      UK/London

Die Abfrageausgabe enthält eine anonyme Spalte, die sich aus der Verkettung der Standortattribute mithilfe der Funktion CONCAT_WS ergibt. (Diese Funktion wurde übrigens in SQL Server 2017 hinzugefügt. Wenn Sie also den Code in einer früheren Version ausführen, können Sie diese Berechnung gerne durch eine alternative Berechnung Ihrer Wahl ersetzen.) Diese Abfrage funktioniert daher nicht eine Tabelle zurückgeben, ganz zu schweigen von einer Relation. Daher ist es nicht zulässig, eine solche Abfrage als Tabellenausdruck/inneren Abfrageteil einer abgeleiteten Tabellendefinition zu verwenden.

Probieren Sie es aus:

SELECT *
FROM ( SELECT empid, firstname, lastname,
         CONCAT_WS(N'/', country, region, city)
       FROM HR.Employees ) AS D;

Sie erhalten die folgende Fehlermeldung:

Msg 8155, Level 16, State 2, Line 50
Für Spalte 4 von „D“ wurde kein Spaltenname angegeben.

Fällt Ihnen nebenbei noch etwas Interessantes an der Fehlermeldung auf? Es beschwert sich über Spalte 4 und hebt den Unterschied zwischen Spalten in SQL und Attributen in der relationalen Theorie hervor.

Die Lösung besteht natürlich darin, sicherzustellen, dass Sie Spalten, die sich aus Berechnungen ergeben, explizit Namen zuweisen. T-SQL unterstützt eine ganze Reihe von Techniken zur Benennung von Spalten. Ich nenne zwei davon.

Sie können eine Inline-Benennungstechnik verwenden, bei der Sie den Zielspaltennamen nach der Berechnung und einer optionalen AS-Klausel zuweisen, wie in < expression > [ AS ] < column name > , etwa so:

SELECT empid, firstname, lastname, custlocation
FROM ( SELECT empid, firstname, lastname,
         CONCAT_WS(N'/', country, region, city) AS custlocation
       FROM HR.Employees ) AS D;

Diese Abfrage generiert die folgende Ausgabe:

empid  firstname  lastname   custlocation
------ ---------- ---------- ----------------
1      Sara       Davis      USA/WA/Seattle
2      Don        Funk       USA/WA/Tacoma
3      Judy       Lew        USA/WA/Kirkland
4      Yael       Peled      USA/WA/Redmond
5      Sven       Mortensen  UK/London
6      Paul       Suurs      UK/London
7      Russell    King       UK/London
8      Maria      Cameron    USA/WA/Seattle
9      Patricia   Doyle      UK/London

Mit dieser Technik ist es sehr einfach, beim Überprüfen des Codes festzustellen, welcher Zielspaltenname welchem ​​Ausdruck zugewiesen ist. Außerdem müssen Sie nur Spalten benennen, die sonst noch keine Namen haben.

Sie können auch eine externere Spaltenbenennungstechnik verwenden, bei der Sie die Zielspaltennamen in Klammern direkt nach dem Namen der abgeleiteten Tabelle angeben, etwa so:

SELECT empid, firstname, lastname, custlocation
FROM ( SELECT empid, firstname, lastname,
         CONCAT_WS(N'/', country, region, city)
       FROM HR.Employees ) AS D(empid, firstname, lastname, custlocation);

Bei dieser Technik müssen Sie jedoch Namen für alle Spalten auflisten – einschließlich derjenigen, die bereits Namen haben. Die Zuordnung der Zielspaltennamen erfolgt positionsbezogen von links nach rechts, d. h. der erste Zielspaltenname repräsentiert den ersten Ausdruck in der SELECT-Liste der inneren Abfrage; der Name der zweiten Zielspalte stellt den zweiten Ausdruck dar; und so weiter.

Beachten Sie, dass im Falle einer Inkonsistenz zwischen den inneren und äußeren Spaltennamen, beispielsweise aufgrund eines Fehlers im Code, der Gültigkeitsbereich der inneren Namen die innere Abfrage ist – oder genauer gesagt die innere Bereichsvariable (hier implizit HR.Employees AS Employees) – und der Gültigkeitsbereich der äußeren Namen ist die äußere Bereichsvariable (in unserem Fall D). Es ist ein bisschen mehr mit dem Scoping von Spaltennamen verbunden, was mit der logischen Abfrageverarbeitung zu tun hat, aber das ist ein Thema für spätere Diskussionen.

Das Potenzial für Fehler bei der externen Benennungssyntax lässt sich am besten anhand eines Beispiels erklären.

Untersuchen Sie die Ausgabe der vorherigen Abfrage mit allen Mitarbeitern aus der HR.Employees-Tabelle. Betrachten Sie dann die folgende Abfrage und versuchen Sie, bevor Sie sie ausführen, herauszufinden, welche Mitarbeiter Sie im Ergebnis erwarten:

SELECT empid, firstname, lastname, custlocation
FROM ( SELECT empid, firstname, lastname,
         CONCAT_WS(N'/', country, region, city)
       FROM HR.Employees
       WHERE lastname LIKE N'D%' ) AS D(empid, lastname, firstname, custlocation)
WHERE firstname LIKE N'D%';

Wenn Sie erwarten, dass die Abfrage einen leeren Satz für die angegebenen Beispieldaten zurückgibt, da es derzeit keine Mitarbeiter mit sowohl einem Nachnamen als auch einem Vornamen gibt, die mit dem Buchstaben D beginnen, übersehen Sie den Fehler im Code.

Führen Sie nun die Abfrage aus und untersuchen Sie die tatsächliche Ausgabe:

empid  firstname  lastname  custlocation
------ ---------- --------- ---------------
1      Davis      Sara      USA/WA/Seattle
9      Doyle      Patricia  UK/London

Was ist passiert?

Die innere Abfrage gibt firstname als zweite Spalte und lastname als dritte Spalte in der SELECT-Liste an. Der Code, der die Zielspaltennamen der abgeleiteten Tabelle in der äußeren Abfrage zuweist, gibt den zweiten Nachnamen und den dritten Vornamen an. Der Code nennt Vorname als Nachname und Nachname als Vorname in der Bereichsvariablen D. Tatsächlich filtern Sie nur Mitarbeiter, deren Nachname mit dem Buchstaben D beginnt. Sie filtern keine Mitarbeiter, die sowohl einen Nachnamen als auch einen Vornamen haben, die beginnen mit dem Buchstaben D.

Die Inline-Aliasing-Syntax ist nicht anfällig für solche Fehler. Zum einen verwenden Sie normalerweise kein Alias ​​für eine Spalte, die bereits einen Namen hat, mit dem Sie zufrieden sind. Zweitens:Selbst wenn Sie einer Spalte, die bereits einen Namen hat, einen anderen Alias ​​zuweisen möchten, ist es nicht sehr wahrscheinlich, dass Sie mit der Syntax AS den falschen Alias ​​zuweisen. Denk darüber nach; wie wahrscheinlich ist es, dass Sie so schreiben werden:

SELECT empid, firstname, lastname, custlocation
FROM ( SELECT empid AS empid, firstname AS lastname, lastname AS firstname,
         CONCAT_WS(N'/', country, region, city) AS custlocation
       FROM HR.Employees
       WHERE lastname LIKE N'D%' ) AS D
WHERE firstname LIKE N'D%';

Offensichtlich nicht sehr wahrscheinlich.

Alle Spaltennamen müssen eindeutig sein

Zurück zu der Tatsache, dass die Überschrift einer Relation eine Menge von Attributen ist, und da ein Attribut durch seinen Namen identifiziert wird, müssen Attributnamen für dieselbe Relation eindeutig sein. In einer bestimmten Abfrage können Sie immer auf ein Attribut verweisen, indem Sie einen zweiteiligen Namen mit dem Bereichsvariablennamen als Qualifizierer verwenden, wie in .. Wenn der Spaltenname ohne Qualifizierer eindeutig ist, können Sie das Namenspräfix der Bereichsvariablen weglassen. Es ist jedoch wichtig, sich daran zu erinnern, was ich zuvor über den Umfang der Spaltennamen gesagt habe. In Code, der einen benannten Tabellenausdruck mit sowohl einer inneren Abfrage (dem Tabellenausdruck) als auch einer äußeren Abfrage beinhaltet, ist der Gültigkeitsbereich der Spaltennamen in der inneren Abfrage die Variablen des inneren Bereichs und der Gültigkeitsbereich der Spaltennamen in der äußeren query sind die Variablen des äußeren Bereichs. Wenn die innere Abfrage mehrere Quelltabellen mit demselben Spaltennamen umfasst, können Sie dennoch eindeutig auf diese Spalten verweisen, indem Sie den Bereichsvariablennamen als Präfix hinzufügen. Wenn Sie den Namen einer Bereichsvariablen nicht explizit zuweisen, wird implizit einer zugewiesen, als ob Sie AS .

verwendet hätten

Betrachten Sie die folgende eigenständige Abfrage als Beispiel:

SELECT C.custid, O.custid, O.orderid
FROM Sales.Customers AS C
  LEFT OUTER JOIN Sales.Orders AS O
    ON C.custid = O.custid;

Diese Abfrage schlägt nicht mit einem doppelten Spaltennamenfehler fehl, da eine custid-Spalte tatsächlich C.custid und die andere O.custid innerhalb des Bereichs der aktuellen Abfrage heißt. Diese Abfrage generiert die folgende Ausgabe:

custid      custid      orderid
----------- ----------- -----------
1           1           10643
1           1           10692
1           1           10702
1           1           10835
1           1           10952
1           1           11011
2           2           10308
2           2           10625
2           2           10759
2           2           10926
...

Versuchen Sie jedoch, diese Abfrage wie folgt als Tabellenausdruck in der Definition einer abgeleiteten Tabelle namens CO zu verwenden:

SELECT *
FROM ( SELECT C.custid, O.custid, O.orderid
       FROM Sales.Customers AS C
         LEFT OUTER JOIN Sales.Orders AS O
           ON C.custid = O.custid ) AS CO;

Was die äußere Abfrage betrifft, haben Sie eine Bereichsvariable mit dem Namen CO, und der Gültigkeitsbereich aller Spaltennamen in der äußeren Abfrage ist diese Bereichsvariable. Die Namen aller Spalten in einer bestimmten Bereichsvariablen (denken Sie daran, dass eine Bereichsvariable eine Beziehungsvariable ist) müssen eindeutig sein. Daher erhalten Sie die folgende Fehlermeldung:

Msg 8156, Level 16, State 1, Line 80
Die Spalte „custid“ wurde mehrfach für „CO“ angegeben.

Die Lösung besteht natürlich darin, den beiden custid-Spalten unterschiedliche Spaltennamen zuzuweisen, was die Bereichsvariable CO anbelangt, etwa so:

SELECT *
FROM ( SELECT C.custid AS custcustid, O.custid AS ordercustid, O.orderid
       FROM Sales.Customers AS C
         LEFT OUTER JOIN Sales.Orders AS O
           ON C.custid = O.custid ) AS CO;

Diese Abfrage generiert die folgende Ausgabe:

custcustid  ordercustid orderid
----------- ----------- -----------
1           1           10643
1           1           10692
1           1           10702
1           1           10835
1           1           10952
1           1           11011
2           2           10308
2           2           10625
2           2           10759
2           2           10926
...

Wenn Sie bewährte Verfahren befolgen, führen Sie die Spaltennamen explizit in der SELECT-Liste der äußersten Abfrage auf. Da es sich nur um eine Bereichsvariable handelt, müssen Sie den zweiteiligen Namen nicht für die äußeren Spaltenreferenzen verwenden. Wenn Sie den zweiteiligen Namen verwenden möchten, stellen Sie den Spaltennamen den Variablennamen CO aus dem äußeren Bereich voran, etwa so:

SELECT CO.custcustid, CO.ordercustid, CO.orderid
FROM ( SELECT C.custid AS custcustid, O.custid AS ordercustid, O.orderid
       FROM Sales.Customers AS C
         LEFT OUTER JOIN Sales.Orders AS O
           ON C.custid = O.custid ) AS CO;

Keine Bestellung

Ich habe ziemlich viel über benannte Tabellenausdrücke und die Reihenfolge zu sagen – genug für einen eigenen Artikel –, also werde ich diesem Thema einen zukünftigen Artikel widmen. Trotzdem wollte ich das Thema hier kurz ansprechen, da es so wichtig ist. Erinnern Sie sich, dass der Rumpf einer Relation eine Menge von Tupeln ist, und ähnlich ist der Rumpf einer Tabelle eine Menge von Zeilen. Eine Menge hat keine Ordnung. Dennoch lässt SQL zu, dass die äußerste Abfrage eine ORDER BY-Klausel hat, die eine Präsentationssortierbedeutung hat, wie die folgende Abfrage demonstriert:

SELECT orderid, val
FROM Sales.OrderValues
ORDER BY val DESC;

Was Sie jedoch verstehen müssen, ist, dass diese Abfrage als Ergebnis keine Beziehung zurückgibt. Selbst aus der Sicht von SQL gibt die Abfrage keine Tabelle als Ergebnis zurück und ist es daher nicht als Tabellenausdruck betrachtet. Folglich ist es unzulässig, eine solche Abfrage als Tabellenausdrucksteil einer abgeleiteten Tabellendefinition zu verwenden.

Versuchen Sie, den folgenden Code auszuführen:

SELECT orderid, val
FROM ( SELECT orderid, val
       FROM Sales.OrderValues
       ORDER BY val DESC ) AS D;

Sie erhalten die folgende Fehlermeldung:

Msg 1033, Level 15, State 1, Line 124
Die ORDER BY-Klausel ist in Ansichten, Inline-Funktionen, abgeleiteten Tabellen, Unterabfragen und allgemeinen Tabellenausdrücken ungültig, es sei denn, TOP, OFFSET oder FOR XML ist ebenfalls angegeben.

Ich werde das es sei denn ansprechen Teil der Fehlermeldung in Kürze.

Wenn Sie möchten, dass die äußerste Abfrage ein geordnetes Ergebnis zurückgibt, müssen Sie die ORDER BY-Klausel in der äußersten Abfrage wie folgt angeben:

SELECT orderid, val
FROM ( SELECT orderid, val
       FROM Sales.OrderValues ) AS D
ORDER BY val DESC;

Wie für das es sei denn Teil der Fehlermeldung; T-SQL unterstützt den proprietären TOP-Filter sowie den Standard-OFFSET-FETCH-Filter. Beide Filter verlassen sich auf eine ORDER BY-Klausel im selben Abfragebereich, um für sie zu definieren, welche obersten Zeilen gefiltert werden sollen. Dies ist leider das Ergebnis einer Falle im Design dieser Funktionen, die die Präsentationsreihenfolge nicht von der Filterreihenfolge trennt. Wie dem auch sei, sowohl Microsoft mit seinem TOP-Filter als auch der Standard mit seinem OFFSET-FETCH-Filter erlauben die Angabe einer ORDER BY-Klausel in der inneren Abfrage, solange sie auch den TOP- bzw. OFFSET-FETCH-Filter angibt. Diese Abfrage ist also gültig, zum Beispiel:

SELECT orderid, val
FROM ( SELECT TOP (3) orderid, val
       FROM Sales.OrderValues
       ORDER BY val DESC ) AS D;

Als ich diese Abfrage auf meinem System ausführte, generierte sie die folgende Ausgabe:

orderid  val
-------- ---------
10865    16387.50
10981    15810.00
11030    12615.05

Es ist jedoch wichtig zu betonen, dass der einzige Grund, warum die ORDER BY-Klausel in der inneren Abfrage zulässig ist, darin besteht, den TOP-Filter zu unterstützen. Das ist die einzige Garantie, die Sie bei der Bestellung erhalten. Da die äußere Abfrage ebenfalls keine ORDER BY-Klausel hat, erhalten Sie trotz des beobachteten Verhaltens keine Garantie für eine bestimmte Präsentationsreihenfolge von dieser Abfrage. Das ist sowohl in T-SQL als auch im Standard der Fall. Hier ist ein Zitat aus dem Standard, der sich mit diesem Teil befasst:

„Die Reihenfolge der Zeilen der durch den angegebenen Tabelle wird nur für den garantiert, der unmittelbar die enthält.“

Wie bereits erwähnt, gibt es noch viel mehr über Tabellenausdrücke und die Reihenfolge zu sagen, was ich in einem zukünftigen Artikel tun werde. Ich werde auch Beispiele liefern, die zeigen, wie das Fehlen der ORDER BY-Klausel in der äußeren Abfrage bedeutet, dass Sie keine Garantien für die Reihenfolge der Präsentation erhalten.

Ein Tabellenausdruck, z. B. eine innere Abfrage in einer abgeleiteten Tabellendefinition, ist also eine Tabelle. Ebenso ist eine abgeleitete Tabelle (im spezifischen Sinne) selbst auch eine Tabelle. Es ist kein Basistisch, aber es ist trotzdem ein Tisch. Gleiches gilt für CTEs, Views und Inline-TVFs. Sie sind keine Basistabellen, eher abgeleitete (im allgemeineren Sinne), aber dennoch Tabellen.

Designfehler

Abgeleitete Tabellen haben zwei Hauptmängel in ihrem Design. Beides hat damit zu tun, dass die abgeleitete Tabelle in der FROM-Klausel der äußeren Abfrage definiert ist.

Ein Designfehler hat mit der Tatsache zu tun, dass Sie, wenn Sie eine abgeleitete Tabelle aus einer äußeren Abfrage abfragen müssen und diese Abfrage wiederum als Tabellenausdruck in einer anderen abgeleiteten Tabellendefinition verwenden müssen, diese Abfragen der abgeleiteten Tabelle am Ende verschachteln. Bei der Datenverarbeitung führt die explizite Verschachtelung von Code mit mehreren Verschachtelungsebenen zu komplexem Code, der schwer zu warten ist.

Hier ist ein sehr einfaches Beispiel, das dies demonstriert:

SELECT orderyear, numcusts
FROM ( SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
       FROM ( SELECT YEAR(orderdate) AS orderyear, custid
              FROM Sales.Orders ) AS D1
       GROUP BY orderyear ) AS D2
WHERE numcusts > 70;

Dieser Code gibt Bestelljahre und die Anzahl der Kunden zurück, die in jedem Jahr Bestellungen aufgegeben haben, nur für Jahre, in denen die Anzahl der Kunden, die Bestellungen aufgegeben haben, größer als 70 war.

Die Hauptmotivation für die Verwendung von Tabellenausdrücken besteht hier darin, mehrfach auf einen Spaltenalias verweisen zu können. Die innerste Abfrage, die als Tabellenausdruck für die abgeleitete Tabelle D1 verwendet wird, fragt die Tabelle Sales.Orders ab und weist dem Ausdruck YEAR(orderdate) den Spaltennamen orderyear zu und gibt auch die Spalte custid zurück. Die Abfrage für D1 gruppiert die Zeilen von D1 nach Bestelljahr und gibt Bestelljahr sowie die eindeutige Anzahl von Kunden zurück, die im betreffenden Jahr Bestellungen aufgegeben haben, die als numcusts bezeichnet werden. Der Code definiert basierend auf dieser Abfrage eine abgeleitete Tabelle namens D2. Die äußerste Abfrage fragt dann D2 ab und filtert nur Jahre, in denen die Anzahl der Kunden, die Bestellungen aufgegeben haben, größer als 70 war.

Ein Versuch, diesen Code zu überprüfen oder bei Problemen Fehler zu beheben, ist aufgrund der mehreren Verschachtelungsebenen schwierig. Anstatt den Code auf natürlichere Weise von oben nach unten zu überprüfen, müssen Sie ihn beginnend mit der innersten Einheit analysieren und allmählich nach außen gehen, da dies praktischer ist.

Der springende Punkt bei der Verwendung abgeleiteter Tabellen in diesem Beispiel war die Vereinfachung des Codes, indem die Notwendigkeit vermieden wird, Ausdrücke zu wiederholen. Aber ich bin mir nicht sicher, ob diese Lösung dieses Ziel erreicht. In diesem Fall ist es wahrscheinlich besser, einige Ausdrücke zu wiederholen, um die Notwendigkeit zu vermeiden, abgeleitete Tabellen insgesamt zu verwenden, etwa so:

SELECT YEAR(orderdate) AS orderyear, COUNT(DISTINCT custid) AS numcusts
FROM Sales.Orders
GROUP BY YEAR(orderdate)
HAVING COUNT(DISTINCT custid) > 70;

Denken Sie daran, dass ich hier ein sehr einfaches Beispiel zur Veranschaulichung zeige. Stellen Sie sich Produktionscode mit mehr Verschachtelungsebenen und längerem, aufwändigerem Code vor, und Sie können sehen, wie die Wartung wesentlich komplizierter wird.

Ein weiterer Fehler im Design abgeleiteter Tabellen hat mit Fällen zu tun, in denen Sie mit mehreren Instanzen derselben abgeleiteten Tabelle interagieren müssen. Betrachten Sie die folgende Abfrage als Beispiel:

SELECT CUR.orderyear, CUR.numorders,
  CUR.numorders - PRV.numorders AS diff
FROM ( SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders
       FROM Sales.Orders
       GROUP BY YEAR(orderdate) ) AS CUR
  LEFT OUTER JOIN
     ( SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders
       FROM Sales.Orders
       GROUP BY YEAR(orderdate) ) AS PRV
    ON CUR.orderyear = PRV.orderyear + 1;

Dieser Code berechnet die Anzahl der in jedem Jahr bearbeiteten Bestellungen sowie die Differenz zum Vorjahr. Ignorieren Sie die Tatsache, dass es einfachere Möglichkeiten gibt, dieselbe Aufgabe mit Fensterfunktionen zu lösen – ich verwende diesen Code, um einen bestimmten Punkt zu veranschaulichen, daher sind die Aufgabe selbst und die verschiedenen Möglichkeiten, sie zu lösen, nicht von Bedeutung.

Ein Join ist ein Tabellenoperator, der seine beiden Eingaben als eine Menge behandelt – was bedeutet, dass es keine Reihenfolge zwischen ihnen gibt. Sie werden als linke und rechte Eingaben bezeichnet, sodass Sie eine davon (oder beide) als beibehaltene Tabelle in einem äußeren Join markieren können, aber dennoch gibt es keine erste und keine zweite unter ihnen. Sie dürfen abgeleitete Tabellen als Join-Eingaben verwenden, aber auf den Bereichsvariablennamen, den Sie der linken Eingabe zuweisen, kann in der Definition der rechten Eingabe nicht zugegriffen werden. Denn beide werden konzeptionell im selben logischen Schritt definiert, quasi zum selben Zeitpunkt. Folglich können Sie beim Verbinden von abgeleiteten Tabellen nicht zwei Bereichsvariablen basierend auf einem Tabellenausdruck definieren. Leider müssen Sie den Code wiederholen und zwei Bereichsvariablen basierend auf zwei identischen Kopien des Codes definieren. Dies erschwert natürlich die Wartbarkeit des Codes und erhöht die Wahrscheinlichkeit von Fehlern. Jede Änderung, die Sie an einem Tabellenausdruck vornehmen, muss auch auf den anderen angewendet werden.

Wie ich in einem zukünftigen Artikel erläutern werde, weisen CTEs in ihrem Design nicht diese beiden Fehler auf, die abgeleitete Tabellen aufweisen.

Tabellenwertkonstruktor

Mit einem Tabellenwertkonstruktor können Sie einen Tabellenwert basierend auf eigenständigen Skalarausdrücken konstruieren. Sie können eine solche Tabelle dann in einer äußeren Abfrage genauso verwenden wie eine abgeleitete Tabelle, die auf einer inneren Abfrage basiert. In einem zukünftigen Artikel bespreche ich lateral abgeleitete Tabellen und Korrelationen im Detail, und ich werde anspruchsvollere Formen von Tabellenwertkonstruktoren zeigen. In diesem Artikel konzentriere ich mich jedoch auf eine einfache Form, die ausschließlich auf in sich geschlossenen Skalarausdrücken basiert.

Die allgemeine Syntax für eine Abfrage eines Tabellenwertkonstruktors lautet wie folgt:

SELECT
) AS
(
);

The table value constructor is defined in the FROM clause of the outer query.

The table’s body is made of a VALUES clause, followed by a comma separated list of pairs of parentheses, each defining a row with a comma separated list of expressions forming the row’s values.

The table’s heading is a comma separated list of the target column names. I’ll talk about a shortcoming of this syntax regarding the table’s heading shortly.

The following code uses a table value constructor to define a table called MyCusts with three columns called custid, companyname and contractdate, and three rows:

SELECT custid, companyname, contractdate
FROM ( VALUES( 2, 'Cust 2', '20200212' ),
             ( 3, 'Cust 3', '20200118' ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate);

The above code is equivalent (both logically and in performance terms) in T-SQL to the following alternative:

SELECT custid, companyname, contractdate
FROM ( SELECT 2, 'Cust 2', '20200212' UNION ALL
       SELECT 3, 'Cust 3', '20200118' UNION ALL
       SELECT 5, 'Cust 5', '20200401' )
       AS MyCusts(custid, companyname, contractdate);

The two are internally algebrized the same way. The syntax with the VALUES clause is standard whereas the syntax with the unified FROMless queries isn’t, hence I prefer the former.

There is a shortcoming in the design of table value constructors in both standard SQL and in T-SQL. Remember that the heading of a relation is made of a set of attributes, and an attribute has a name and a type name. In the table value constructor’s syntax, you specify the column names, but not their data types. Suppose that you need the custid column to be of a SMALLINT type, the companyname column of a VARCHAR(50) type, and the contractdate column of a DATE type. It would have been good if we were able to define the column types as part of the definition of the table’s heading, like so (this syntax isn’t supported):

SELECT custid, companyname, contractdate
FROM ( VALUES( 2, 'Cust 2', '20200212' ),
             ( 3, 'Cust 3', '20200118' ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid SMALLINT, companyname VARCHAR(50), contractdate DATE);

That’s of course just wishful thinking.

The way it works in T-SQL, is that each literal that is based on a constant has a predetermined type irrespective of context. For instance, can you guess what the types of the following literals are:

  • 1
  • 2147483647
  • 2147483648
  • 1E
  • '1E'
  • '20200212'

Is 1 considered BIT, INT, SMALLINT, other?

Is 1E considered VARBINARY(1), VARCHAR(2), other?

Is '20200212' considered DATE, DATETIME, VARCHAR(8), CHAR(8), other?

There’s a simple trick to figure out the default type of a literal, using the SQL_VARIANT_PROPERTY function with the 'BaseType' property, like so:

SELECT SQL_VARIANT_PROPERTY(2147483648, 'BaseType');

What happens is that SQL Server implicitly converts the literal to SQL_VARIANT—since that’s what the function expects—but preserves its base type. It then reports the base type as requested.

Similarly, you can query other properties of the input value, like the maximum length (MaxLength), Precision, Scale, and so on.

Try it with the aforementioned literal values, and you will get the following:

  • 1:INT
  • 2147483647:INT
  • 2147483648:NUMERIC(10, 0)
  • 1E:FLOAT
  • '1E':VARCHAR(2)
  • '20200212':VARCHAR(8)

As you can see, SQL Server has default assumptions about the data type, maximum length, precision, scale, and so on.

There are some cases where you need to specify a literal of a certain type, but you cannot do it directly in T-SQL. For example, you cannot specify a literal of the following types directly:BIT, TINYINT, BIGINT, all date and time types, and quite a few others. Unfortunately, T-SQL doesn’t provide a selector property for its types, which would have served exactly the needed purpose of selecting a value of the given type. Of course, you can always convert an expression’s type explicitly using the CAST or CONVERT function, as in CAST(5 AS SMALLINT). If you don’t, SQL Server will sometimes need to implicitly convert some of your expressions to a different type based on its implicit conversion rules. For example, when you try to compare values of different types, e.g., WHERE datecol ='20200212', assuming datecol is of a DATE type. Another example is when you specify a literal in an INSERT or an UPDATE statement, and the literal’s type is different than the target column’s type.

If all this is not confusing enough, set operators like UNION ALL rely on data type precedence to define the target column types—and remember, a table value constructor is algebrized like a series of UNION ALL operations. Consider the table value constructor shown earlier:

SELECT custid, companyname, contractdate
FROM ( VALUES( 2, 'Cust 2', '20200212' ),
             ( 3, 'Cust 3', '20200118' ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate);

Each literal here has a predetermined type. 2, 3 and 5 are all of an INT type, so clearly the custid target column type is INT. If you had the values 1000000000, 3000000000 and 2000000000, the first and the third are considered INT and the second is considered NUMERIC(10, 0). According to data type precedence NUMERIC (same as DECIMAL) is stronger than INT, hence in such a case the target column type would be NUMERIC(10, 0).

If you want to figure out which data types SQL Server chooses for the target columns in your table value constructor, you have a few options. One is to use a SELECT INTO statement to write the table value constructor’s data into a temporary table, and then query the metadata for the temporary table, like so:

SELECT custid, companyname, contractdate
INTO #MyCusts
FROM ( VALUES( 2, 'Cust 2', '20200212' ),
             ( 3, 'Cust 3', '20200118' ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate);
 
SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlength
FROM tempdb.sys.columns
WHERE OBJECT_ID = OBJECT_ID(N'tempdb..#MyCusts');

Here’s the output of this code:

colname       typename   maxlength
------------- ---------- ---------
custid        int        4
companyname   varchar    6
contractdate  varchar    8

You can then drop the temporary table for cleanup:

DROP TABLE IF EXISTS #MyCusts;

Another option is to use the SQL_VARIANT_PROPERTY, which I mentioned earlier, like so:

SELECT TOP (1)
  SQL_VARIANT_PROPERTY(custid, 'BaseType')        AS custid_typename,
  SQL_VARIANT_PROPERTY(custid, 'MaxLength')       AS custid_maxlength,
  SQL_VARIANT_PROPERTY(companyname, 'BaseType')   AS companyname_typename,
  SQL_VARIANT_PROPERTY(companyname, 'MaxLength')  AS companyname_maxlength,
  SQL_VARIANT_PROPERTY(contractdate, 'BaseType')  AS contractdate_typename,
  SQL_VARIANT_PROPERTY(contractdate, 'MaxLength') AS contractdate_maxlength
FROM ( VALUES( 2, 'Cust 2', '20200212' ),
             ( 3, 'Cust 3', '20200118' ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate);

This code generates the following output (formatted for readability):

custid_typename       custid_maxlength
--------------------  ---------------- 
int                   4                

companyname_typename  companyname_maxlength 
--------------------  --------------------- 
varchar               6                     

contractdate_typename contractdate_maxlength
--------------------- ----------------------
varchar               8

So, what if you need to control the types of the target columns? As mentioned earlier, say you need custid to be SMALLINT, companyname VARCHAR(50), and contractdate DATE.

Don’t be misled to think that it’s enough to explicitly convert just one row’s values. If a corresponding value’s type in any other row is considered stronger, it would dictate the target column’s type. Hier ist ein Beispiel, das dies demonstriert:

SELECT custid, companyname, contractdate
INTO #MyCusts1
FROM ( VALUES( CAST(2 AS SMALLINT), CAST('Cust 2' AS VARCHAR(50)), CAST('20200212' AS DATE)),
             ( 3, 'Cust 3', '20200118' ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate);
 
SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlength
FROM tempdb.sys.columns
WHERE OBJECT_ID = OBJECT_ID(N'tempdb..#MyCusts1');

Dieser Code generiert die folgende Ausgabe:

colname       typename  maxlength
------------- --------- ---------
custid        int       4
companyname   varchar   50
contractdate  date      3

Notice that the type for custid is INT.

The same applies never mind which row’s values you explicitly convert, if you don’t convert all of them. For example, here the code explicitly converts the types of the values in the second row:

SELECT custid, companyname, contractdate
INTO #MyCusts2
FROM ( VALUES( 2, 'Cust 2', '20200212'),
             ( CAST(3 AS SMALLINT), CAST('Cust 3' AS VARCHAR(50)), CAST('20200118' AS DATE) ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate);
 
SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlength
FROM tempdb.sys.columns
WHERE OBJECT_ID = OBJECT_ID(N'tempdb..#MyCusts2');

Dieser Code generiert die folgende Ausgabe:

colname       typename  maxlength
------------- --------- ---------
custid        int       4
companyname   varchar   50
contractdate  date      3

As you can see, custid is still of an INT type.

You basically have two main options. One is to explicitly convert all values, like so:

SELECT custid, companyname, contractdate
INTO #MyCusts3
FROM ( VALUES( CAST(2 AS SMALLINT), CAST('Cust 2' AS VARCHAR(50)), CAST('20200212' AS DATE)),
             ( CAST(3 AS SMALLINT), CAST('Cust 3' AS VARCHAR(50)), CAST('20200118' AS DATE)),
             ( CAST(5 AS SMALLINT), CAST('Cust 5' AS VARCHAR(50)), CAST('20200401' AS DATE)) )
       AS MyCusts(custid, companyname, contractdate);
 
SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlength
FROM tempdb.sys.columns
WHERE OBJECT_ID = OBJECT_ID(N'tempdb..#MyCusts3');

This code generates the following output, showing all target columns have the desired types:

colname       typename  maxlength
------------- --------- ---------
custid        smallint  2
companyname   varchar   50
contractdate  date      3

That’s a lot of coding, though. Another option is to apply the conversions in the SELECT list of the query against the table value constructor, and then define a derived table against the query that applies the conversions, like so:

SELECT custid, companyname, contractdate
INTO #MyCusts4
FROM ( SELECT
         CAST(custid AS SMALLINT) AS custid,
         CAST(companyname AS VARCHAR(50)) AS companyname,
         CAST(contractdate AS DATE) AS contractdate
       FROM ( VALUES( 2, 'Cust 2', '20200212' ),
                    ( 3, 'Cust 3', '20200118' ),
                    ( 5, 'Cust 5', '20200401' ) )
              AS D(custid, companyname, contractdate) ) AS MyCusts;
 
SELECT name AS colname, TYPE_NAME(system_type_id) AS typename, max_length AS maxlength
FROM tempdb.sys.columns
WHERE OBJECT_ID = OBJECT_ID(N'tempdb..#MyCusts4');

Dieser Code generiert die folgende Ausgabe:

colname       typename  maxlength
------------- --------- ---------
custid        smallint  2
companyname   varchar   50
contractdate  date      3

The reasoning for using the additional derived table is due to how logical query processing is designed. The SELECT clause is evaluated after FROM, WHERE, GROUP BY and HAVING. By applying the conversions in the SELECT list of the inner query, you allow expressions in all clauses of the outermost query to interact with the columns with the proper types.

Back to our wishful thinking, clearly, it would be good if we ever get a syntax that allows explicit control of the types in the definition of the table value constructor’s heading, like so:

SELECT custid, companyname, contractdate
FROM ( VALUES( 2, 'Cust 2', '20200212' ),
             ( 3, 'Cust 3', '20200118' ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid SMALLINT, companyname VARCHAR(50), contractdate DATE);

When you’re done, run the following code for cleanup:

DROP TABLE IF EXISTS #MyCusts1, #MyCusts2, #MyCusts3, #MyCusts4;

Used in modification statements

T-SQL allows you to modify data through table expressions. That’s true for derived tables, CTEs, views and inline TVFs. What gets modified in practice is some underlying base table that is used by the table expression. I have much to say about modifying data through table expressions, and I will in a future article dedicated to this topic. Here, I just wanted to briefly mention the types of modification statements that specifically support derived tables, and provide the syntax.

Derived tables can be used as the target table in DELETE and UPDATE statements, and also as the source table in the MERGE statement (in the USING clause). They cannot be used in the TRUNCATE statement, and as the target in the INSERT and MERGE statements.

For the DELETE and UPDATE statements, the syntax for defining the derived table is a bit awkward. You don’t define the derived table in the DELETE and UPDATE clauses, like you would expect, but rather in a separate FROM clause. You then specify the derived table name in the DELETE or UPDATE clause.

Here’s the general syntax of a DELETE statement against a derived table:

DELETE [ FROM ]

FROM (
) [ AS ]
[ () ]
[ WHERE ];

As an example (don’t actually run it), the following code deletes all US customers with a customer ID that is greater than the minimum for the same region (the region column represents the state for US customers):

DELETE FROM UC
FROM ( SELECT *, ROW_NUMBER() OVER(PARTITION BY region ORDER BY custid) AS rownum
       FROM Sales.Customers
       WHERE country = N'USA' ) AS UC
WHERE rownum > 1;

Here’s the general syntax of an UPDATE statement against a derived table:

UPDATE

SET
FROM (
) [ AS ]
[ () ]
[ WHERE ];

As you can see, from the perspective of the definition of the derived table, it’s quite similar to the syntax of the DELETE statement.

As an example, the following code changes the company names of US customers to one using the format N'USA Cust ' + rownum, where rownum represents a position based on customer ID ordering:

BEGIN TRAN;
 
UPDATE UC
  SET companyname = newcompanyname
    OUTPUT
      inserted.custid,
      deleted.companyname AS oldcompanyname,
      inserted.companyname AS newcompanyname
FROM ( SELECT custid, companyname,
         N'USA Cust ' + CAST(ROW_NUMBER() OVER(ORDER BY custid) AS NVARCHAR(10)) AS newcompanyname 
       FROM Sales.Customers
       WHERE country = N'USA' ) AS UC;
 
ROLLBACK TRAN;

The code applies the update in a transaction that it then rolls back so that the change won't stick.

This code generates the following output, showing both the old and the new company names:

custid  oldcompanyname  newcompanyname
------- --------------- ----------------
32      Customer YSIQX  USA Cust 1
36      Customer LVJSO  USA Cust 2
43      Customer UISOJ  USA Cust 3
45      Customer QXPPT  USA Cust 4
48      Customer DVFMB  USA Cust 5
55      Customer KZQZT  USA Cust 6
65      Customer NYUHS  USA Cust 7
71      Customer LCOUJ  USA Cust 8
75      Customer XOJYP  USA Cust 9
77      Customer LCYBZ  USA Cust 10
78      Customer NLTYP  USA Cust 11
82      Customer EYHKM  USA Cust 12
89      Customer YBQTI  USA Cust 13

That’s it for now on the topic.

Zusammenfassung

Derived tables are one of the four main types of named table expressions that T-SQL supports. In this article I focused on the logical aspects of derived tables. I described the syntax for defining them and their scope.

Remember that a table expression is a table and as such, all of its columns must have names, all column names must be unique, and the table has no order.

The design of derived tables incurs two main flaws. In order to query one derived table from another, you need to nest your code, causing it to be more complex to maintain and troubleshoot. If you need to interact with multiple occurrences of the same table expression, using derived tables you are forced to duplicate your code, which hurts the maintainability of your solution.

You can use a table value constructor to define a table based on self-contained expressions as opposed to querying some existing base tables.

You can use derived tables in modification statements like DELETE and UPDATE, though the syntax for doing so is a bit awkward.