sql >> Database teknologi >  >> RDS >> Database

Grundlæggende om tabeludtryk, del 2 – Afledte tabeller, logiske overvejelser

I sidste måned gav jeg en baggrund til tabeludtryk i T-SQL. Jeg forklarede konteksten ud fra relationsteorien og SQL-standarden. Jeg forklarede, hvordan en tabel i SQL er et forsøg på at repræsentere en relation fra relationsteori. Jeg forklarede også, at et relationelt udtryk er et udtryk, der opererer på en eller flere relationer som input og resulterer i en relation. På samme måde i SQL er et tabeludtryk et udtryk, der opererer på en eller flere inputtabeller og resulterer i en tabel. Udtrykket kan være en forespørgsel, men behøver ikke at være det. For eksempel kan udtrykket være en tabelværdikonstruktør, som jeg vil forklare senere i denne artikel. Jeg forklarede også, at jeg i denne serie fokuserer på fire specifikke typer af navngivne tabeludtryk, som T-SQL understøtter:afledte tabeller, almindelige tabeludtryk (CTE'er), visninger og indlejrede tabelværdier (TVF'er).

Hvis du har arbejdet med T-SQL i noget tid, er du sikkert stødt ind i en del tilfælde, hvor du enten skulle bruge tabeludtryk, eller det var på en eller anden måde mere bekvemt sammenlignet med alternative løsninger, der ikke bruger dem. Her er blot nogle få eksempler på use cases, der kommer til at tænke på:

  • Skab en modulopbygget løsning ved at opdele komplekse opgaver i trin, der hver repræsenteres af et forskelligt tabeludtryk.
  • Blanding af resultater af grupperede forespørgsler og detaljer, hvis du beslutter dig for ikke at bruge vinduesfunktioner til dette formål.
  • Logisk forespørgselsbehandling håndterer forespørgselsklausuler i følgende rækkefølge:FROM>WHERE>GROUP BY>HAVING>SELECT>ORDER BY. Som følge heraf er kolonnealiasser, som du definerer i SELECT-udtrykket, kun tilgængelige for ORDER BY-udtrykket på samme niveau af indlejring. De er ikke tilgængelige for resten af ​​forespørgselsklausulerne. Med tabeludtryk kan du genbruge aliaser, som du definerer i en indre forespørgsel i en hvilken som helst klausul af den ydre forespørgsel, og på denne måde undgå gentagelse af lange/komplekse udtryk.
  • Vinduesfunktioner kan kun vises i en forespørgsels SELECT- og ORDER BY-klausuler. Med tabeludtryk kan du tildele et alias til et udtryk baseret på en vinduesfunktion og derefter bruge dette alias i en forespørgsel mod tabeludtrykket.
  • En PIVOT-operator involverer tre elementer:gruppering, spredning og aggregering. Denne operatør identificerer grupperingselementet implicit ved eliminering. Ved hjælp af et tabeludtryk kan du projicere præcis de tre elementer, der skal være involveret, og få den ydre forespørgsel til at bruge tabeludtrykket som PIVOT-operatørens inputtabel og dermed kontrollere, hvilket element der er grupperingselementet.
  • Ændringer med TOP understøtter ikke en ORDER BY-klausul. Du kan kontrollere, hvilke rækker der vælges indirekte ved at definere et tabeludtryk baseret på en SELECT-forespørgsel med TOP- eller OFFSET-FETCH-filteret og et ORDER BY-udtryk og anvende modifikationen mod tabeludtrykket.

Dette er langt fra en udtømmende liste. Jeg vil demonstrere nogle af ovenstående use cases og andre i denne serie. Jeg ville bare nævne nogle use cases her for at illustrere, hvor vigtige tabeludtryk er i vores T-SQL-kode, og hvorfor det er umagen værd at investere i at forstå deres grundlæggende principper godt.

I denne måneds artikel fokuserer jeg specifikt på den logiske behandling af afledte tabeller.

I mine eksempler vil jeg bruge en prøvedatabase kaldet TSQLV5. Du kan finde scriptet, der opretter og udfylder det her, og dets ER-diagram her.

Afledte tabeller

Udtrykket afledt tabel bruges i SQL og T-SQL med mere end én betydning. Så først vil jeg gøre det klart, hvilken jeg henviser til i denne artikel. Jeg henviser til en specifik sprogkonstruktion, som du typisk definerer, men ikke kun, i FROM-klausulen i en ydre forespørgsel. Jeg vil snart give syntaksen for denne konstruktion.

Den mere generelle brug af begrebet afledt tabel i SQL er modstykket til en afledt relation fra relationsteori. En afledt relation er en resultatrelation, der er afledt af en eller flere inputbaserelationer ved at anvende relationelle operatorer fra relationel algebra som projektion, skæringspunkt og andre til disse basisrelationer. På samme måde i generel forstand er en afledt tabel i SQL en resultattabel, der er afledt fra en eller flere basistabeller ved at evaluere udtryk mod disse inputbasetabeller.

Som en sidebemærkning tjekkede jeg, hvordan SQL-standarden definerer en basistabel, og jeg var straks ked af, at jeg generede det.

4.15.2 Basistabeller

En basistabel er enten en vedvarende basistabel eller en midlertidig tabel.

En persistent basistabel er enten en almindelig persistent basistabel eller en systemversionstabel.

En almindelig basistabel er enten en almindelig vedvarende basistabel eller en midlertidig tabel.”

Tilføjet her uden yderligere kommentarer...

I T-SQL kan du oprette en basistabel med en CREATE TABLE-sætning, men der er andre muligheder, f.eks. SELECT INTO og DECLARE @T AS TABLE.

Her er standardens definition for afledte tabeller i generel betydning:

4.15.3 Afledte tabeller

En afledt tabel er en tabel, der er afledt direkte eller indirekte fra en eller flere andre tabeller ved evalueringen af ​​et udtryk, såsom en , , eller

. Et kan indeholde en valgfri . Rækkefølgen af ​​rækkerne i tabellen specificeret af er kun garanteret for , der umiddelbart indeholder ."

Der er et par interessante ting at bemærke her om afledte tabeller i generel forstand. Man har at gøre med kommentaren om bestilling. Jeg kommer til denne senere i artiklen. En anden er, at en afledt tabel i SQL kan være et gyldigt selvstændigt tabeludtryk, men det behøver ikke at være det. For eksempel repræsenterer følgende udtryk en afledt tabel, og er betragtes også som et gyldigt selvstændigt tabeludtryk (du kan køre det):

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

Omvendt repræsenterer følgende udtryk en afledt tabel, men er det ikke et gyldigt selvstændigt tabeludtryk:

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

T-SQL understøtter en række tabeloperatorer, der giver en afledt tabel, men som ikke understøttes som selvstændige udtryk. Disse er:JOIN, PIVOT, UNPIVOT og APPLY. Du har brug for en klausul, som de kan fungere inden for (typisk FROM, men også MERGE-sætningens USING-klausul) og en værtsforespørgsel.

Herefter vil jeg bruge udtrykket afledt tabel til at beskrive en mere specifik sprogkonstruktion og ikke i den generelle betydning beskrevet ovenfor.

Syntaks

En afledt tabel kan defineres som en del af en ydre SELECT-sætning i dens FROM-sætning. Det kan også defineres som en del af DELETE- og UPDATE-sætningerne i deres FROM-sætning og som en del af en MERGE-sætning i dens USING-sætning. Jeg vil give flere detaljer om syntaksen, når den bruges i modifikationserklæringer senere i denne artikel.

Her er syntaksen for en forenklet SELECT-forespørgsel mod en afledt tabel:

VÆLG
FRA ( ) [ AS ] [ () ];

Den afledte tabeldefinition vises, hvor en basistabel normalt kan forekomme, i den ydre forespørgsels FROM-klausul. Det kan være et input til en tabeloperator som JOIN, APPLY, PIVOT og UNPIVOT. Når den bruges som det rigtige input til en APPLY-operator, tillades -delen af ​​den afledte tabel at have korrelationer til kolonner fra en ydre tabel (mere om dette i en dedikeret fremtidig artikel i serien). Ellers skal tabeludtrykket være selvstændigt.

Den ydre sætning kan have alle de sædvanlige forespørgselselementer. I et tilfælde af SELECT-sætning:WHERE, GROUP BY, HAVING, ORDER BY og som nævnt tabeloperatorer i FROM-sætningen.

Her er et eksempel på en simpel forespørgsel mod en afledt tabel, der repræsenterer kunder i USA:

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

Denne forespørgsel genererer følgende output:

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

Der er tre hoveddele at identificere i en sætning, der involverer en afledt tabeldefinition:

  1. Tabeludtrykket (den indre forespørgsel)
  2. Det afledte tabelnavn, eller mere præcist, hvad der i relationsteori betragtes som en intervalvariabel
  3. Det ydre udsagn

Tabeludtrykket formodes at repræsentere en tabel og skal som sådan opfylde visse krav, som en normal forespørgsel ikke nødvendigvis behøver at opfylde. Jeg vil snart give detaljerne i afsnittet "Et tabeludtryk er en tabel".

Hvad angår det målafledte tabelnavn; en almindelig antagelse blandt T-SQL-udviklere er, at det blot er et navn eller et alias, som du tildeler måltabellen. På samme måde kan du overveje følgende forespørgsel:

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

Også her er den almindelige antagelse, at AS C blot er en måde at omdøbe, eller alias, tabellen Kunder med henblik på denne forespørgsel, startende med det logiske forespørgselsbehandlingstrin, hvor navnet tildeles, og fremefter. Men fra relationsteoriens synspunkt er der en dybere mening med, hvad C repræsenterer. C er det, der er kendt som en intervalvariabel. C er en afledt relationsvariabel, der går over tuplerne i inputrelationsvariablen Kunder. I ovenstående eksempel går C over tuplerne i Kunder og evaluerer prædikatet land =N'USA'. Tupler, for hvilke prædikatet vurderes til at være sandt, bliver en del af resultatrelationen C.

Et tabeludtryk er en tabel

Med den baggrund, som jeg har givet indtil videre, burde det, jeg nu skal forklare, ikke være overraskende.

delen af ​​en afledt tabeldefinition er en tabel . Det er tilfældet, selvom det er udtrykt som en forespørgsel. Kan du huske lukningsegenskaben ved relationel algebra? Det samme gælder for resten af ​​de førnævnte navngivne tabeludtryk (CTE'er, views og inline TVF'er). Som du allerede har lært, SQLs tabel er modstykket til relationsteoriens relation , omend ikke et perfekt modstykke. Et tabeludtryk skal således opfylde visse krav for at sikre, at resultatet er en tabel – dem, som en forespørgsel, der ikke bruges som et tabeludtryk, ikke nødvendigvis skal. Her er tre specifikke krav:

  • Alle tabeludtrykkets kolonner skal have navne
  • Alle tabeludtrykkets kolonnenavne skal være unikke
  • Tabeludtrykkets rækker har ingen rækkefølge

Lad os nedbryde disse krav en efter en og diskutere relevansen for både relationsteori og SQL.

Alle kolonner skal have navne

Husk, at en relation har en overskrift og en krop. Overskriften på en relation er et sæt attributter (kolonner i SQL). En attribut har et navn og et typenavn og identificeres ved sit navn. En forespørgsel, der ikke bruges som et tabeludtryk, behøver ikke nødvendigvis at tildele navne til alle målkolonner. Overvej følgende forespørgsel som et eksempel:

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

Denne forespørgsel genererer følgende output:

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

Forespørgselsoutputtet har en anonym kolonne, der er et resultat af sammenkædningen af ​​lokationsattributterne ved hjælp af CONCAT_WS-funktionen. (Denne funktion blev i øvrigt tilføjet i SQL Server 2017, så hvis du kører koden i en tidligere version, er du velkommen til at erstatte denne beregning med en alternativ beregning efter eget valg.) Denne forespørgsel gør det derfor ikke returnere en tabel, for ikke at tale om en relation. Derfor er det ikke gyldigt at bruge en sådan forespørgsel som tabeludtrykket/den indre forespørgselsdel af en afledt tabeldefinition.

Prøv det:

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

Du får følgende fejlmeddelelse:

Msg 8155, Level 16, State 2, Line 50
Intet kolonnenavn blev angivet for kolonne 4 i 'D'.

Som en sidebemærkning, bemærker du noget interessant ved fejlmeddelelsen? Den klager over kolonne 4 og fremhæver forskellen mellem kolonner i SQL og attributter i relationsteori.

Løsningen er selvfølgelig at sikre, at du eksplicit tildeler navne til kolonner, der er resultatet af beregninger. T-SQL understøtter en del kolonnenavngivningsteknikker. Jeg vil nævne to af dem.

Du kan bruge en indlejret navngivningsteknik, hvor du tildeler målkolonnenavnet efter beregningen og en valgfri AS-klausul, som i <-udtryk> [AS ] , sådan:

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

Denne forespørgsel genererer følgende output:

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

Ved at bruge denne teknik er det meget nemt, når man gennemgår koden, at fortælle hvilket målkolonnenavn der er tildelt hvilket udtryk. Du behøver også kun at navngive kolonner, der ellers ikke allerede har navne.

Du kan også bruge en mere ekstern kolonnenavngivningsteknik, hvor du angiver målkolonnenavnene i parentes lige efter det afledte tabelnavn, som sådan:

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 denne teknik skal du dog angive navne for alle kolonner - inklusive dem, der allerede har navne. Tildelingen af ​​målkolonnenavnene udføres efter position fra venstre mod højre, dvs. det første målkolonnenavn repræsenterer det første udtryk i den indre forespørgsels SELECT-liste; det anden målkolonnenavn repræsenterer det andet udtryk; og så videre.

Bemærk, at i tilfælde af uoverensstemmelse mellem de indre og ydre kolonnenavne, f.eks. på grund af en fejl i koden, er omfanget af de indre navne den indre forespørgsel – eller mere præcist den indre områdevariabel (her implicit HR.Employees AS Employees) - og omfanget af de ydre navne er den ydre områdevariabel (D i vores tilfælde). Der er lidt mere involveret i omfanget af kolonnenavne, der har at gøre med logisk forespørgselsbehandling, men det er et punkt til senere diskussioner.

Potentialet for fejl med den eksterne navngivningssyntaks forklares bedst med et eksempel.

Undersøg resultatet af den forrige forespørgsel med det fulde sæt af medarbejdere fra HR.Employees-tabellen. Overvej derefter følgende forespørgsel, og før du kører den, prøv at finde ud af, hvilke medarbejdere du forventer at se i resultatet:

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

Hvis du forventer, at forespørgslen returnerer et tomt sæt for de givne eksempeldata, da der i øjeblikket ikke er nogen medarbejdere med både et efternavn og et fornavn, der starter med bogstavet D, mangler du fejlen i koden.

Kør nu forespørgslen, og undersøg det faktiske output:

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

Hvad skete der?

Den indre forespørgsel angiver fornavn som anden kolonne og efternavn som tredje kolonne i SELECT-listen. Koden, der tildeler den afledte tabels målkolonnenavne i den ydre forespørgsel, angiver efternavn andet og fornavn tredje. Kodenavnene fornavn som efternavn og efternavn som fornavn i intervalvariablen D. Faktisk filtrerer du bare medarbejdere, hvis efternavn starter med bogstavet D. Du filtrerer ikke medarbejdere med både et efternavn og et fornavn, der starter med bogstavet D.

Den inline aliasing syntaks er ikke tilbøjelig til sådanne fejl. For det første kalder du normalt ikke en kolonne, der allerede har et navn, du er tilfreds med. For det andet, selvom du ønsker at tildele et andet alias til en kolonne, der allerede har et navn, er det ikke særlig sandsynligt, at du med syntaksen AS vil tildele det forkerte alias. Tænk over det; hvor sandsynligt er det, at du skriver sådan her:

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

Det er åbenbart ikke særlig sandsynligt.

Alle kolonnenavne skal være unikke

Tilbage til det faktum, at overskriften på en relation er et sæt attributter, og givet at en attribut er identificeret ved navn, skal attributnavne være unikke for den samme relation. I en given forespørgsel kan du altid henvise til en attribut ved hjælp af et todelt navn med områdevariabelnavnet som kvalifikation, som i .. Når kolonnenavnet uden kvalifikatoren er utvetydigt, kan du udelade præfikset for områdevariabelnavnet. Det, der dog er vigtigt at huske, er det, jeg sagde tidligere om rækkevidden af ​​kolonnenavnene. I kode, der involverer et navngivet tabeludtryk, med både en indre forespørgsel (tabeludtrykket) og en ydre forespørgsel, er omfanget af kolonnenavnene i den indre forespørgsel de indre områdevariabler og omfanget af kolonnenavnene i den ydre forespørgsel forespørgsel er de ydre områdevariabler. Hvis den indre forespørgsel involverer flere kildetabeller med det samme kolonnenavn, kan du stadig henvise til disse kolonner på en utvetydig måde ved at tilføje områdevariabelnavnet som et præfiks. Hvis du ikke eksplicit tildeler et områdevariabelnavn, får du et implicit tildelt, som hvis du brugte AS .

Overvej følgende selvstændige forespørgsel som et eksempel:

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

Denne forespørgsel mislykkes ikke med en dublet kolonnenavnsfejl, da den ene custid-kolonne faktisk hedder C.custid og den anden O.custid inden for den aktuelle forespørgsels rækkevidde. Denne forespørgsel genererer følgende output:

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

Prøv dog at bruge denne forespørgsel som et tabeludtryk i definitionen af ​​en afledt tabel med navnet CO, som sådan:

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;

For så vidt angår den ydre forespørgsel, har du én områdevariabel ved navn CO, og omfanget af alle kolonnenavne i den ydre forespørgsel er den områdevariabel. Navnene på alle kolonner i en given intervalvariabel (husk, en intervalvariabel er en relationsvariabel) skal være unikke. Derfor får du følgende fejl:

Msg 8156, Level 16, State 1, Line 80
Kolonnen 'custid' blev angivet flere gange for 'CO'.

Rettelsen er selvfølgelig at tildele forskellige kolonnenavne til de to custid-kolonner, hvad angår intervalvariablen CO, som sådan:

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;

Denne forespørgsel genererer følgende output:

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

Hvis du følger god praksis, angiver du eksplicit kolonnenavnene i den yderste forespørgsels SELECT-liste. Da der kun er én intervalvariabel involveret, behøver du ikke bruge det todelte navn til de ydre kolonnereferencer. Hvis du ønsker at bruge det todelte navn, skal du præfikse kolonnenavnene med det ydre områdevariabelnavn CO, sådan:

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 ordre

Der er ret meget, jeg har at sige om navngivne tabeludtryk og rækkefølge - nok til en artikel i sig selv - så jeg vil dedikere en fremtidig artikel til dette emne. Alligevel ville jeg kort berøre emnet her, da det er så vigtigt. Husk, at kroppen af ​​en relation er et sæt af tupler, og på samme måde er kroppen af ​​en tabel et sæt rækker. Et sæt har ingen rækkefølge. Alligevel tillader SQL, at den yderste forespørgsel har en ORDER BY-klausul, der tjener en præsentationsordringsbetydning, som følgende forespørgsel demonstrerer:

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

Hvad du dog skal forstå er, at denne forespørgsel ikke returnerer en relation som et resultat. Selv fra SQLs perspektiv returnerer forespørgslen ikke en tabel som et resultat, og derfor er det ikke betragtes som et tabeludtryk. Det er derfor ugyldigt at bruge en sådan forespørgsel som tabeludtryksdelen af ​​en afledt tabeldefinition.

Prøv at køre følgende kode:

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

Du får følgende fejlmeddelelse:

Msg 1033, Level 15, State 1, Line 124
ORDER BY-sætningen er ugyldig i visninger, inline-funktioner, afledte tabeller, underforespørgsler og almindelige tabeludtryk, medmindre TOP, OFFSET eller FOR XML også er angivet.

Jeg tager fat på medmindre del af fejlmeddelelsen snart.

Hvis du ønsker, at den yderste forespørgsel skal returnere et ordnet resultat, skal du angive ORDER BY-klausulen i den yderste forespørgsel, sådan:

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

Med hensyn til medmindre del af fejlmeddelelsen; T-SQL understøtter det proprietære TOP-filter samt standard OFFSET-FETCH-filteret. Begge filtre er afhængige af en ORDER BY-klausul i det samme forespørgselsomfang for at definere for dem, hvilke øverste rækker der skal filtreres. Dette er desværre resultatet af en fælde i designet af disse funktioner, som ikke adskiller præsentationsbestilling fra filterbestilling. Hvorom alting er, både Microsoft med dets TOP-filter og standarden med dets OFFSET-FETCH-filter tillader at specificere en ORDER BY-klausul i den indre forespørgsel, så længe den også specificerer henholdsvis TOP- eller OFFSET-FETCH-filteret. Så denne forespørgsel er gyldig, for eksempel:

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

Da jeg kørte denne forespørgsel på mit system, genererede den følgende output:

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

Det, der dog er vigtigt at understrege, er, at den eneste grund til, at ORDER BY-klausulen er tilladt i den indre forespørgsel, er at understøtte TOP-filteret. Det er den eneste garanti for, at du når, hvad bestilling angår. Da den ydre forespørgsel ikke også har en ORDER BY-klausul, får du ikke garanti for nogen specifik præsentationsbestilling fra denne forespørgsel, uanset hvad den observerede adfærd er. Det er både tilfældet i T-SQL såvel som i standarden. Her er et citat fra standarden, der omhandler denne del:

"Rækkefølgen af ​​rækkerne i tabellen specificeret af er kun garanteret for , der umiddelbart indeholder ."

Som nævnt er der meget mere at sige om bordudtryk og rækkefølge, hvilket jeg vil gøre i en fremtidig artikel. Jeg vil også give eksempler, der viser, hvordan manglen på ORDER BY-klausul i den ydre forespørgsel betyder, at du ikke får nogen præsentationsbestillingsgarantier.

Så et tabeludtryk, f.eks. en indre forespørgsel i en afledt tabeldefinition, er en tabel. På samme måde er en afledt tabel (i den specifikke betydning) i sig selv også en tabel. Det er ikke et basisbord, men det er ikke desto mindre et bord. Det samme gælder for CTE'er, views og inline TVF'er. De er ikke basistabeller, snarere afledte (i mere generel forstand), men de er ikke desto mindre tabeller.

Designfejl

Afledte tabeller har to hovedmangler i deres design. Begge har at gøre med det faktum, at den afledte tabel er defineret i FROM-delen af ​​den ydre forespørgsel.

En designfejl har at gøre med det faktum, at hvis du har brug for at forespørge en afledt tabel fra en ydre forespørgsel, og til gengæld bruge den forespørgsel som et tabeludtryk i en anden afledt tabeldefinition, ender du med at indlejre disse afledte tabelforespørgsler. Inden for databehandling har eksplicit indlejring af kode, der involverer flere niveauer af indlejring, tendens til at resultere i kompleks kode, som er svær at vedligeholde.

Her er et meget grundlæggende eksempel, der viser dette:

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;

Denne kode returnerer ordreår og antallet af kunder, der afgav ordrer i løbet af hvert år, kun for år, hvor antallet af kunder, der afgav ordrer, var større end 70.

Hovedmotivationen for at bruge tabeludtryk her er for at kunne henvise til et kolonnealias flere gange. Den inderste forespørgsel, der bruges som et tabeludtryk for den afledte tabel D1, forespørger i Sales.Orders-tabellen, og tildeler kolonnenavnet ordreår til udtrykket YEAR(orderdate), og returnerer også custid-kolonnen. Forespørgslen mod D1 grupperer rækkerne fra D1 efter ordreår og returnerer ordreår samt det distinkte antal kunder, der har afgivet ordrer i løbet af det pågældende år, kaldet numcust. Koden definerer en afledt tabel kaldet D2 baseret på denne forespørgsel. Den yderste forespørgsel end forespørgsler D2 og filtrerer kun år, hvor antallet af kunder, der afgav ordrer, var større end 70.

Et forsøg på at gennemgå denne kode eller fejlfinde den i tilfælde af problemer er vanskelig på grund af de flere niveauer af indlejring. I stedet for at gennemgå koden på den mere naturlige top-til-bund måde, ser du ud til, at du skal analysere den, begyndende med den inderste enhed og gradvist gå udad, da det er mere praktisk.

Hele pointen med at bruge afledte tabeller i dette eksempel var at forenkle koden ved at undgå behovet for at gentage udtryk. Men jeg er ikke sikker på, at denne løsning når dette mål. I dette tilfælde er det nok bedre at gentage nogle udtryk og helt undgå behovet for at bruge afledte tabeller, som sådan:

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

Husk, at jeg viser et meget simpelt eksempel her til illustrationsformål. Forestil dig produktionskode med flere niveauer af indlejring og med længere, mere udførlig kode, og du kan se, hvordan det bliver væsentligt mere kompliceret at vedligeholde.

En anden fejl i designet af afledte tabeller har at gøre med tilfælde, hvor du skal interagere med flere forekomster af den samme afledte tabel. Overvej følgende forespørgsel som et eksempel:

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;

Denne kode beregner antallet af ordrer, der behandles i hvert år, samt forskellen fra det foregående år. Ignorer det faktum, at der er enklere måder at opnå den samme opgave med vinduesfunktioner - jeg bruger denne kode til at illustrere et bestemt punkt, så selve opgaven og de forskellige måder at løse den på er ikke væsentlige.

En joinforbindelse er en tabeloperator, der behandler sine to input som et sæt - hvilket betyder, at der ikke er nogen rækkefølge blandt dem. De omtales som venstre og højre input, så du kan markere en af ​​dem (eller begge) som en bevaret tabel i en ydre sammenføjning, men alligevel er der ingen første og anden blandt dem. Det er tilladt at bruge afledte tabeller som join-input, men det områdevariabelnavn, som du tildeler det venstre input, er ikke tilgængeligt i definitionen af ​​det højre input. Det er fordi begge er konceptuelt defineret i det samme logiske trin, som om det var på samme tidspunkt. Når du forbinder afledte tabeller, kan du derfor ikke definere to intervalvariabler baseret på ét tabeludtryk. Desværre er du nødt til at gentage koden og definere to intervalvariabler baseret på to identiske kopier af koden. Dette komplicerer naturligvis vedligeholdelsen af ​​koden og øger sandsynligheden for fejl. Hver ændring, du foretager i et tabeludtryk, skal også anvendes på det andet.

Som jeg vil forklare i en fremtidig artikel, pådrager CTE'er i deres design ikke disse to fejl, som afledte tabeller pådrager sig.

Tabelværdikonstruktør

En tabelværdikonstruktør giver dig mulighed for at konstruere en tabelværdi baseret på selvstændige skalarudtryk. Du kan derefter bruge en sådan tabel i en ydre forespørgsel, ligesom du bruger en afledt tabel, der er baseret på en indre forespørgsel. I en fremtidig artikel diskuterer jeg laterale afledte tabeller og korrelationer i detaljer, og jeg vil vise mere sofistikerede former for tabelværdikonstruktører. I denne artikel vil jeg dog fokusere på en simpel form, der udelukkende er baseret på selvstændige skalarudtryk.

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

Denne kode genererer følgende 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');

Denne kode genererer følgende 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');

Denne kode genererer følgende 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);

Når du er færdig, skal du køre følgende kode til oprydning:

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.

Oversigt

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. Softwaregennemgang – Stellar Repair til MS SQL

  2. Oracle (Gamle?) Joins - Et værktøj/script til konvertering?

  3. Kortlægning af PostgreSQL-tekst[][]-type og Java-type

  4. Returner kun numeriske værdier i MariaDB