sql >> Databasteknik >  >> RDS >> Database

Grundläggande tabelluttryck, del 2 – Härledda tabeller, logiska överväganden

Förra månaden gav jag en bakgrund till tabelluttryck i T-SQL. Jag förklarade sammanhanget från relationsteorin och SQL-standarden. Jag förklarade hur en tabell i SQL är ett försök att representera en relation från relationsteori. Jag förklarade också att ett relationsuttryck är ett uttryck som verkar på en eller flera relationer som input och resulterar i en relation. På liknande sätt, i SQL, är ett tabelluttryck ett uttryck som verkar på en eller flera inmatningstabeller och resulterar i en tabell. Uttrycket kan vara en fråga, men behöver inte vara det. Till exempel kan uttrycket vara en tabellvärdekonstruktor, som jag kommer att förklara senare i den här artikeln. Jag förklarade också att jag i den här serien fokuserar på fyra specifika typer av namngivna tabelluttryck som T-SQL stöder:härledda tabeller, vanliga tabelluttryck (CTE), vyer och inline-tabellvärderade funktioner (TVF).

Om du har arbetat med T-SQL ett tag har du förmodligen snubblat in i en hel del fall där du antingen var tvungen att använda tabelluttryck, eller så var det på något sätt bekvämare jämfört med alternativa lösningar som inte använder dem. Här är bara några exempel på användningsfall som kommer att tänka på:

  • Skapa en modulär lösning genom att dela upp komplexa uppgifter i steg som vart och ett representeras av olika tabelluttryck.
  • Blanda resultat av grupperade frågor och detaljer om du bestämmer dig för att inte använda fönsterfunktioner för detta ändamål.
  • Logisk frågebehandling hanterar frågesatser i följande ordning:FROM>WHERE>GROUP BY>HAVING>SELECT>ORDER BY. Som ett resultat, på samma nivå av kapsling, är kolumnalias som du definierar i SELECT-satsen endast tillgängliga för ORDER BY-satsen. De är inte tillgängliga för resten av frågeklausulerna. Med tabelluttryck kan du återanvända alias som du definierar i en inre fråga i valfri sats i den yttre frågan, och på så sätt undvika upprepning av långa/komplexa uttryck.
  • Fönsterfunktioner kan endast visas i en frågas SELECT- och ORDER BY-satser. Med tabelluttryck kan du tilldela ett alias till ett uttryck baserat på en fönsterfunktion och sedan använda det aliaset i en fråga mot tabelluttrycket.
  • En PIVOT-operatör innefattar tre element:gruppering, spridning och aggregering. Denna operatör identifierar grupperingselementet implicit genom eliminering. Med hjälp av ett tabelluttryck kan du projicera exakt de tre element som ska vara inblandade och låta den yttre frågan använda tabelluttrycket som PIVOT-operatorns inmatningstabell och på så sätt styra vilket element som är grupperingselementet.
  • Ändringar med TOP stöder inte en ORDER BY-sats. Du kan styra vilka rader som väljs indirekt genom att definiera ett tabelluttryck baserat på en SELECT-fråga med TOP- eller OFFSET-FETCH-filtret och en ORDER BY-sats, och tillämpa modifieringen mot tabelluttrycket.

Detta är långt ifrån en uttömmande lista. Jag kommer att visa några av ovanstående användningsfall och andra i den här serien. Jag ville bara nämna några användningsfall här för att illustrera hur viktiga tabelluttryck är i vår T-SQL-kod, och varför det är värt att investera i att förstå deras grunder väl.

I denna månads artikel fokuserar jag på den logiska behandlingen av härledda tabeller specifikt.

I mina exempel kommer jag att använda en exempeldatabas som heter TSQLV5. Du kan hitta skriptet som skapar och fyller det här, och dess ER-diagram här.

Härledda tabeller

Termen härledd tabell används i SQL och T-SQL med mer än en betydelse. Så först vill jag göra det klart vilken jag syftar på i den här artikeln. Jag syftar på en specifik språkkonstruktion som du definierar typiskt, men inte bara, i FROM-satsen i en yttre fråga. Jag kommer att tillhandahålla syntaxen för denna konstruktion inom kort.

Den mer allmänna användningen av termen härledd tabell i SQL är motsvarigheten till en härledd relation från relationsteori. En härledd relation är en resultatrelation som härleds från en eller flera indatabasrelationer, genom att tillämpa relationsoperatorer från relationalgebra som projektion, skärningspunkt och andra på dessa basrelationer. På liknande sätt, i allmän mening, är en härledd tabell i SQL en resultattabell som härleds från en eller flera bastabeller, genom att utvärdera uttryck mot dessa indatabastabeller.

För övrigt kollade jag hur SQL-standarden definierar en bastabell och blev direkt ledsen att jag störde mig.

4.15.2 Bastabeller

En bastabell är antingen en beständig bastabell eller en tillfällig tabell.

En beständig bastabell är antingen en vanlig beständig bastabell eller en systemversionstabell.

En vanlig bastabell är antingen en vanlig beständig bastabell eller en temporär tabell.”

Tillagt här utan ytterligare kommentarer...

I T-SQL kan du skapa en bastabell med en CREATE TABLE-sats, men det finns andra alternativ, t.ex. SELECT INTO och DECLARE @T AS TABLE.

Här är standardens definition för härledda tabeller i allmän mening:

4.15.3 Härledda tabeller

En härledd tabell är en tabell som härleds direkt eller indirekt från en eller flera andra tabeller genom utvärderingen av ett uttryck, till exempel en , , eller

. Ett kan innehålla en valfri . Ordningen av raderna i tabellen som anges av garanteras endast för som omedelbart innehåller ."

Det finns ett par intressanta saker att notera här om härledda tabeller i allmän mening. Man har att göra med kommentaren om beställning. Jag kommer till detta senare i artikeln. En annan är att en härledd tabell i SQL kan vara ett giltigt fristående tabelluttryck, men det behöver inte vara det. Till exempel representerar följande uttryck en härledd tabell och är anses också vara ett giltigt fristående tabelluttryck (du kan köra det):

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

Omvänt representerar följande uttryck en härledd tabell, men är det inte ett giltigt fristående tabelluttryck:

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

T-SQL stöder ett antal tabelloperatorer som ger en härledd tabell, men som inte stöds som fristående uttryck. Dessa är:JOIN, PIVOT, UNPIVOT och APPLY. Du behöver en klausul för att de ska kunna arbeta inom (vanligtvis FROM, men också MERGE-satsens USING-sats), och en värdfråga.

Från och med nu kommer jag att använda termen härledd tabell för att beskriva en mer specifik språkkonstruktion och inte i den allmänna betydelsen som beskrivs ovan.

Syntax

En härledd tabell kan definieras som en del av en yttre SELECT-sats i dess FROM-sats. Det kan också definieras som en del av DELETE- och UPDATE-satser i deras FROM-sats, och som en del av en MERGE-sats i dess USING-sats. Jag kommer att ge mer information om syntaxen när den används i modifieringssatser senare i den här artikeln.

Här är syntaxen för en förenklad SELECT-fråga mot en härledd tabell:

VÄLJ
FRÅN ( ) [ AS ] [ () ];

Den härledda tabelldefinitionen visas där en bastabell normalt kan förekomma, i den yttre frågans FROM-sats. Det kan vara en input till en tabelloperator som JOIN, APPLY, PIVOT och UNPIVOT. När den används som rätt indata till en APPLY-operator, tillåts

-delen av den härledda tabellen ha korrelationer till kolumner från en yttre tabell (mer om detta i en dedikerad framtida artikel i serien). Annars måste tabelluttrycket vara fristående.

Den yttre satsen kan ha alla vanliga frågeelement. I ett SELECT-satsfall:WHERE, GROUP BY, HAVING, ORDER BY och som nämnts, tabelloperatorer i FROM-satsen.

Här är ett exempel på en enkel fråga mot en härledd tabell som representerar kunder i USA:

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

Den här frågan genererar följande utdata:

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

Det finns tre huvuddelar att identifiera i ett uttalande som involverar en härledd tabelldefinition:

  1. Tabelluttrycket (den inre frågan)
  2. Det härledda tabellnamnet, eller mer exakt, det som i relationsteorin anses vara en intervallvariabel
  3. Det yttre påståendet

Tabelluttrycket är tänkt att representera en tabell och måste som sådant uppfylla vissa krav som en normal fråga inte nödvändigtvis behöver uppfylla. Jag kommer att ge detaljerna inom kort i avsnittet "Ett tabelluttryck är en tabell".

När det gäller det målhärledda tabellnamnet; ett vanligt antagande bland T-SQL-utvecklare är att det bara är ett namn eller alias som du tilldelar måltabellen. På samma sätt, överväg följande fråga:

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

Även här är det vanliga antagandet att AS C bara är ett sätt att byta namn på, eller alias, tabellen Kunder för den här frågan, med början med det logiska frågebearbetningssteget där namnet tilldelas och framåt. Men från relationsteorins synvinkel finns det en djupare mening med vad C representerar. C är vad som kallas en intervallvariabel. C är en härledd relationsvariabel som sträcker sig över tuplarna i indatarelationsvariabeln Kunder. I exemplet ovan sträcker sig C över tuplarna i Kunder och utvärderar predikatlandet =N'USA'. Tupler för vilka predikatet utvärderas till sant blir en del av resultatrelationen C.

Ett tabelluttryck är en tabell

Med den bakgrund som jag har gett hittills borde det jag ska förklara härnäst inte vara någon överraskning.

delen av en härledd tabelldefinition är en tabell . Så är fallet även om det uttrycks som en fråga. Kommer du ihåg closure-egenskapen för relationalgebra? Detsamma gäller för resten av de ovannämnda namngivna tabelluttrycken (CTEs, views och inline TVFs). Som du redan har lärt dig, SQLs tabell är motsvarigheten till relationsteorins relation , om än inte en perfekt motsvarighet. Således måste ett tabelluttryck uppfylla vissa krav för att säkerställa att resultatet är en tabell – sådana som en fråga som inte används som ett tabelluttryck inte nödvändigtvis behöver. Här är tre specifika krav:

  • Alla tabelluttryckets kolumner måste ha namn
  • Alla tabelluttryckets kolumnnamn måste vara unika
  • Tabelluttryckets rader har ingen ordning

Låt oss bryta ner dessa krav en efter en och diskutera relevansen för både relationsteori och SQL.

Alla kolumner måste ha namn

Kom ihåg att en relation har en rubrik och en kropp. Rubriken för en relation är en uppsättning attribut (kolumner i SQL). Ett attribut har ett namn och ett typnamn och identifieras med sitt namn. En fråga som inte används som ett tabelluttryck behöver inte nödvändigtvis tilldela namn till alla målkolumner. Betrakta följande fråga som ett exempel:

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

Den här frågan genererar följande utdata:

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

Frågeutgången har en anonym kolumn som är resultatet av sammanlänkning av platsattributen med hjälp av CONCAT_WS-funktionen. (Förresten, den här funktionen lades till i SQL Server 2017, så om du kör koden i en tidigare version, ersätt gärna den här beräkningen med en alternativ beräkning som du väljer.) Den här frågan gör det därför inte returnera en tabell, för att inte tala om en relation. Därför är det inte giltigt att använda en sådan fråga som tabelluttrycket/inre frågedelen av en härledd tabelldefinition.

Prova det:

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

Du får följande felmeddelande:

Msg 8155, Level 16, State 2, Line 50
Inget kolumnnamn angavs för kolumn 4 i 'D'.

Lägger du märke till något intressant med felmeddelandet? Den klagar på kolumn 4 och belyser skillnaden mellan kolumner i SQL och attribut i relationsteori.

Lösningen är naturligtvis att se till att du uttryckligen tilldelar namn till kolumner som är resultatet av beräkningar. T-SQL stöder en hel del kolumnnamnstekniker. Jag ska nämna två av dem.

Du kan använda en inline-namngivningsteknik där du tilldelar målkolumnnamnet efter beräkningen och en valfri AS-sats, som i <-uttryck> [AS ] , som så:

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

Den här frågan genererar följande utdata:

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

Med den här tekniken är det mycket enkelt när man granskar koden för att se vilket målkolumnnamn som är tilldelat vilket uttryck. Dessutom behöver du bara namnge kolumner som inte redan har namn annars.

Du kan också använda en mer extern kolumnnamngivningsteknik där du anger målkolumnnamnen inom parentes direkt efter det härledda tabellnamnet, som så:

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);

Med den här tekniken måste du dock lista namn för alla kolumner – inklusive de som redan har namn. Tilldelningen av målkolumnnamnen görs efter position, från vänster till höger, dvs. det första målkolumnnamnet representerar det första uttrycket i den inre frågans SELECT-lista; det andra målkolumnnamnet representerar det andra uttrycket; och så vidare.

Observera att i händelse av inkonsekvens mellan de inre och yttre kolumnnamnen, t.ex. på grund av en bugg i koden, är omfattningen av de inre namnen den inre frågan – eller, mer exakt, den inre intervallvariabeln (här implicit HR.Employees AS-anställda) – och omfattningen av de yttre namnen är den yttre intervallvariabeln (D i vårt fall). Det är lite mer involverat i omfattningen av kolumnnamn som har att göra med logisk frågebehandling, men det är ett föremål för senare diskussioner.

Potentialen för buggar med den externa namnsyntaxen förklaras bäst med ett exempel.

Undersök resultatet av den föregående frågan, med hela uppsättningen anställda från tabellen HR.Employees. Tänk sedan på följande fråga och försök ta reda på vilka anställda du förväntar dig att se i resultatet innan du kör den:

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%';

Om du förväntar dig att frågan ska returnera en tom uppsättning för givna exempeldata, eftersom det för närvarande inte finns några anställda med både ett efternamn och ett förnamn som börjar med bokstaven D, saknar du felet i koden.

Kör nu frågan och undersök den faktiska utdata:

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

Vad hände?

Den inre frågan anger förnamn som den andra kolumnen och efternamn som den tredje kolumnen i SELECT-listan. Koden som tilldelar den härledda tabellens målkolumnnamn i den yttre frågan anger efternamn andra och förnamn tredje. Kodnamnen förnamn som efternamn och efternamn som förnamn i intervallvariabeln D. I praktiken filtrerar du bara anställda vars efternamn börjar med bokstaven D. Du filtrerar inte anställda med både ett efternamn och ett förnamn som börjar med bokstaven D.

Syntaxen för inline alias är inte benägen för sådana buggar. För det första kallar du normalt inte en kolumn som redan har ett namn du är nöjd med. För det andra, även om du vill tilldela ett annat alias för en kolumn som redan har ett namn, är det inte särskilt troligt att du med syntaxen AS tilldelar fel alias. Tänk på det; hur troligt är det att du skriver så här:

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%';

Uppenbarligen inte särskilt troligt.

Alla kolumnnamn måste vara unika

Tillbaka till det faktum att rubriken för en relation är en uppsättning attribut, och givet att ett attribut identifieras med namn, måste attributnamn vara unika för samma relation. I en given fråga kan du alltid referera till ett attribut med ett tvådelat namn med intervallvariabelns namn som kvalificerare, som i .. När kolumnnamnet utan kvalificeraren är entydigt kan du utelämna intervallvariabelns namnprefix. Vad som är viktigt att komma ihåg är det jag sa tidigare om omfattningen av kolumnnamnen. I kod som involverar ett namngivet tabelluttryck, med både en inre fråga (tabelluttrycket) och en yttre fråga, är omfattningen av kolumnnamnen i den inre frågan de inre intervallvariablerna och omfattningen av kolumnnamnen i den yttre query är de yttre intervallvariablerna. Om den inre frågan involverar flera källtabeller med samma kolumnnamn kan du fortfarande referera till dessa kolumner på ett entydigt sätt genom att lägga till intervallvariabelns namn som ett prefix. Om du inte explicit tilldelar ett intervallvariabelnamn får du ett implicit tilldelat, som om du använde AS .

Betrakta följande fristående fråga som ett exempel:

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

Den här frågan misslyckas inte med ett duplicerat kolumnnamnsfel eftersom en custid-kolumn faktiskt heter C.custid och den andra O.custid inom den aktuella frågans räckvidd. Den här frågan genererar följande utdata:

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
...

Försök dock att använda den här frågan som ett tabelluttryck i definitionen av en härledd tabell med namnet CO, så här:

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;

När det gäller den yttre frågan har du en intervallvariabel som heter CO, och omfattningen av alla kolumnnamn i den yttre frågan är den intervallvariabeln. Namnen på alla kolumner i en given intervallvariabel (kom ihåg att en intervallvariabel är en relationsvariabel) måste vara unika. Därför får du följande felmeddelande:

Msg 8156, Level 16, State 1, Line 80
Kolumnen 'custid' specificerades flera gånger för 'CO'.

Fixningen är naturligtvis att tilldela olika kolumnnamn till de två custid-kolumnerna när det gäller intervallvariabeln CO, som så:

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;

Den här frågan genererar följande utdata:

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
...

Om du följer god praxis listar du uttryckligen kolumnnamnen i den yttersta frågans SELECT-lista. Eftersom det bara finns en intervallvariabel inblandad, behöver du inte använda det tvådelade namnet för de yttre kolumnreferenserna. Om du vill använda det tvådelade namnet, prefixer du kolumnnamnen med det yttre intervallvariabelnamnet CO, så här:

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;

Ingen beställning

Det finns ganska mycket jag har att säga om namngivna tabelluttryck och ordning – tillräckligt för en artikel i sig – så jag kommer att ägna en framtida artikel till detta ämne. Ändå ville jag beröra ämnet kort här eftersom det är så viktigt. Kom ihåg att kroppen av en relation är en uppsättning tupler, och på samma sätt är kroppen i en tabell en uppsättning rader. Ett set har ingen ordning. Ändå tillåter SQL att den yttersta frågan har en ORDER BY-klausul som tjänar en presentationsordningsinnebörd, vilket följande fråga visar:

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

Vad du dock behöver förstå är att den här frågan inte returnerar en relation som ett resultat. Även ur SQLs perspektiv returnerar frågan inte en tabell som ett resultat, och därför är det inte betraktas som ett tabelluttryck. Följaktligen är det ogiltigt att använda en sådan fråga som tabelluttrycksdelen av en härledd tabelldefinition.

Testa att köra följande kod:

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

Du får följande felmeddelande:

Msg 1033, Level 15, State 1, Line 124
ORDER BY-satsen är ogiltig i vyer, inline-funktioner, härledda tabeller, underfrågor och vanliga tabelluttryck, om inte TOP, OFFSET eller FOR XML också anges.

Jag tar upp om inte del av felmeddelandet inom kort.

Om du vill att den yttersta frågan ska returnera ett ordnat resultat, måste du ange ORDER BY-satsen i den yttersta frågan, så här:

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

När det gäller om inte del av felmeddelandet; T-SQL stöder det proprietära TOP-filtret såväl som standardfiltret OFFSET-FETCH. Båda filtren förlitar sig på en ORDER BY-sats i samma frågeomfång för att definiera för dem vilka översta rader som ska filtreras. Detta är tyvärr resultatet av en fälla i utformningen av dessa funktioner, som inte skiljer presentationsbeställning från filterbeställning. Hur som helst, både Microsoft med sitt TOP-filter och standarden med sitt OFFSET-FETCH-filter tillåter att specificera en ORDER BY-sats i den inre frågan så länge som den också specificerar TOP- respektive OFFSET-FETCH-filtret. Så den här frågan är giltig, till exempel:

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

När jag körde den här frågan på mitt system genererade den följande utdata:

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

Vad som dock är viktigt att betona är att det enda skälet till att ORDER BY-satsen är tillåten i den inre frågan är att stödja TOP-filtret. Det är den enda garantin att du kommer när det gäller beställning. Eftersom den yttre frågan inte heller har en ORDER BY-klausul, får du ingen garanti för någon specifik presentationsordning från denna fråga, trots det observerade beteendet. Det är både fallet i T-SQL, såväl som i standarden. Här är ett citat från standarden som tar upp denna del:

"Ordningen av raderna i tabellen som specificeras av garanteras endast för som omedelbart innehåller ."

Som nämnts finns det mycket mer att säga om bordsuttryck och ordning, vilket jag kommer att göra i en framtida artikel. Jag kommer också att ge exempel som visar hur avsaknaden av ORDER BY-klausul i den yttre frågan innebär att du inte får några garantier för presentationsbeställning.

Så ett tabelluttryck, t.ex. en inre fråga i en härledd tabelldefinition, är en tabell. På samma sätt är en härledd tabell (i den specifika meningen) i sig också en tabell. Det är inte ett basbord, men det är ändå ett bord. Detsamma gäller CTE:er, visningar och inline TVF:er. De är inte bastabeller, snarare härledda sådana (i mer allmän mening), men de är ändå tabeller.

Designfel

Härledda tabeller har två huvudsakliga brister i sin design. Båda har att göra med det faktum att den härledda tabellen är definierad i FROM-satsen i den yttre frågan.

Ett designfel har att göra med det faktum att om du behöver fråga en härledd tabell från en yttre fråga, och i sin tur använda den frågan som ett tabelluttryck i en annan härledd tabelldefinition, slutar du med att kapsla dessa härledda tabellfrågor. Inom datorer tenderar explicit kapsling av kod som involverar flera nivåer av kapsling att resultera i komplex kod som är svår att underhålla.

Här är ett mycket grundläggande exempel som visar detta:

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;

Denna kod returnerar beställningsår och antalet kunder som gjort beställningar under varje år, endast för år där antalet kunder som beställt var fler än 70.

Den främsta motiveringen för att använda tabelluttryck här är att kunna referera till ett kolumnalias flera gånger. Den innersta frågan som används som ett tabelluttryck för den härledda tabellen D1 frågar efter tabellen Sales.Orders och tilldelar kolumnnamnet orderyear till uttrycket YEAR(orderdatum), och returnerar även custid-kolumnen. Frågan mot D1 grupperar raderna från D1 efter orderår och returnerar orderår såväl som det distinkta antalet kunder som lagt beställningar under året i fråga under alias som numcust. Koden definierar en härledd tabell som heter D2 baserat på denna fråga. Den yttersta frågan än frågar D2 och filtrerar endast år där antalet kunder som gjorde beställningar var fler än 70.

Ett försök att granska den här koden eller felsöka den i händelse av problem är knepigt på grund av de flera nivåerna av kapsling. Istället för att granska koden på det mer naturliga sättet från topp till botten, måste du analysera den med början på den innersta enheten och gradvis gå utåt, eftersom det är mer praktiskt.

Hela poängen med att använda härledda tabeller i det här exemplet var att förenkla koden genom att undvika behovet av att upprepa uttryck. Men jag är inte säker på att den här lösningen uppnår detta mål. I det här fallet är det förmodligen bättre att du upprepar vissa uttryck och undviker behovet av att använda härledda tabeller helt och hållet, som så:

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

Tänk på att jag visar ett mycket enkelt exempel här i illustrationssyfte. Föreställ dig produktionskod med fler nivåer av kapsling och med längre, mer utarbetad kod, så kan du se hur det blir betydligt mer komplicerat att underhålla.

En annan brist i utformningen av härledda tabeller har att göra med fall där du behöver interagera med flera instanser av samma härledda tabell. Betrakta följande fråga som ett exempel:

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;

Den här koden beräknar antalet beställningar som behandlas varje år, såväl som skillnaden från föregående år. Ignorera det faktum att det finns enklare sätt att uppnå samma uppgift med fönsterfunktioner – jag använder den här koden för att illustrera en viss punkt, så själva uppgiften och de olika sätten att lösa den är inte signifikanta.

En join är en tabelloperator som behandlar sina två ingångar som en uppsättning – vilket betyder att det inte finns någon ordning bland dem. De kallas för vänster och höger ingångar så att du kan markera en av dem (eller båda) som en bevarad tabell i en yttre sammanfogning, men det finns fortfarande ingen första och andra bland dem. Du får använda härledda tabeller som join-ingångar, men intervallvariabelnamnet som du tilldelar den vänstra ingången är inte tillgängligt i definitionen av den högra ingången. Det beror på att båda är konceptuellt definierade i samma logiska steg, som vid samma tidpunkt. När du sammanfogar härledda tabeller kan du följaktligen inte definiera två intervallvariabler baserat på ett tabelluttryck. Tyvärr måste du upprepa koden och definiera två intervallvariabler baserat på två identiska kopior av koden. Detta komplicerar förstås kodens underhållbarhet och ökar sannolikheten för buggar. Varje ändring som du gör i ett tabelluttryck måste tillämpas på det andra också.

Som jag kommer att förklara i en framtida artikel, ådrar sig inte CTE:er i sin design dessa två brister som härledda tabeller ådrar sig.

Tabellvärdekonstruktor

En tabellvärdekonstruktor låter dig konstruera ett tabellvärde baserat på fristående skalära uttryck. Du kan sedan använda en sådan tabell i en yttre fråga precis som du använder en härledd tabell som är baserad på en inre fråga. I en framtida artikel diskuterar jag lateralt härledda tabeller och korrelationer i detalj, och jag kommer att visa mer sofistikerade former av tabellvärdekonstruktörer. In this article, though, I’ll focus on a simple form that is based purely on self-contained scalar expressions.

The general syntax for a query against a table value constructor is as follows:

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. Here’s an example demonstrating this:

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');

This code generates the following output:

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');

This code generates the following output:

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');

This code generates the following output:

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.

Sammanfattning

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.


  1. Uppdateringar av PostgreSQL-testverktyg med benchmark-arkiv

  2. Ta reda på vilken partition ett givet värde skulle mappas till i SQL Server (T-SQL)

  3. Upprätthålla ordning i MySQL IN-frågan

  4. generera dagar från datumintervall