Vorige maand heb ik een achtergrond gegeven voor tabeluitdrukkingen in T-SQL. Ik legde de context uit vanuit de relationele theorie en de SQL-standaard. Ik heb uitgelegd hoe een tabel in SQL een poging is om een relatie uit de relationele theorie weer te geven. Ik heb ook uitgelegd dat een relationele uitdrukking een uitdrukking is die werkt op een of meer relaties als input en resulteert in een relatie. Evenzo is in SQL een tabeluitdrukking een uitdrukking die op een of meer invoertabellen werkt en resulteert in een tabel. De expressie kan een query zijn, maar hoeft dat niet te zijn. De expressie kan bijvoorbeeld een tabelwaardeconstructor zijn, zoals ik later in dit artikel zal uitleggen. Ik heb ook uitgelegd dat ik me in deze serie concentreer op vier specifieke typen benoemde tabeluitdrukkingen die T-SQL ondersteunt:afgeleide tabellen, algemene tabeluitdrukkingen (CTE's), weergaven en inline tabelwaardefuncties (TVF's).
Als je al een tijdje met T-SQL werkt, ben je waarschijnlijk een flink aantal gevallen tegengekomen waarin je ofwel tabeluitdrukkingen moest gebruiken, of dat het op de een of andere manier handiger was in vergelijking met alternatieve oplossingen die ze niet gebruiken. Hier zijn slechts een paar voorbeelden voor gebruiksscenario's die bij u opkomen:
Creëer een modulaire oplossing door complexe taken op te splitsen in stappen, elk vertegenwoordigd door een andere tabeluitdrukking.
Resultaten van gegroepeerde zoekopdrachten en details combineren, voor het geval u besluit de vensterfuncties voor dit doel niet te gebruiken.
Logische queryverwerking verwerkt query-clausules in de volgende volgorde:FROM>WHERE>GROUP BY>HAVING>SELECT>ORDER BY. Als gevolg hiervan zijn kolomaliassen die u definieert in de SELECT-component op hetzelfde niveau van nesten alleen beschikbaar voor de ORDER BY-component. Ze zijn niet beschikbaar voor de rest van de query-clausules. Met tabelexpressies kunt u aliassen hergebruiken die u in een inner query in elke clausule van de outer query definieert, en op deze manier voorkomen dat lange/complexe expressies worden herhaald.
Vensterfuncties kunnen alleen voorkomen in de SELECT- en ORDER BY-clausules van een query. Met tabeluitdrukkingen kunt u een alias toewijzen aan een uitdrukking op basis van een vensterfunctie, en die alias vervolgens gebruiken in een query voor de tabeluitdrukking.
Een PIVOT-operator omvat drie elementen:groeperen, spreiden en aggregeren. Deze operator identificeert het groeperingselement impliciet door eliminatie. Met behulp van een tabeluitdrukking kunt u precies de drie elementen projecteren die geacht worden erbij betrokken te zijn, en de buitenste query de tabeluitdrukking laten gebruiken als de invoertabel van de PIVOT-operator, en zo bepalen welk element het groeperingselement is.
Aanpassingen met TOP ondersteunen geen ORDER BY-clausule. U kunt bepalen welke rijen indirect worden gekozen door een tabeluitdrukking te definiëren op basis van een SELECT-query met het TOP- of OFFSET-FETCH-filter en een ORDER BY-clausule, en de wijziging toepassen op de tabeluitdrukking.
Dit is verre van een volledige lijst. Ik zal enkele van de bovenstaande use-cases en andere in deze serie demonstreren. Ik wilde hier enkele gebruiksvoorbeelden noemen om te illustreren hoe belangrijk tabeluitdrukkingen zijn in onze T-SQL-code, en waarom het de moeite waard is om te investeren in een goed begrip van hun grondbeginselen.
In het artikel van deze maand richt ik me specifiek op de logische behandeling van afgeleide tabellen.
In mijn voorbeelden gebruik ik een voorbeelddatabase met de naam TSQLV5. Je kunt het script dat het maakt en vult hier vinden, en het ER-diagram hier.
Afgeleide tabellen
De term afgeleide tabel wordt in SQL en T-SQL met meer dan één betekenis gebruikt. Dus eerst wil ik duidelijk maken welke ik bedoel in dit artikel. Ik verwijs naar een specifieke taalconstructie die u meestal, maar niet alleen, definieert in de FROM-clausule van een buitenste query. Ik zal binnenkort de syntaxis voor deze constructie leveren.
Het meer algemene gebruik van de term afgeleide tabel in SQL is de tegenhanger van een afgeleide relatie uit de relationele theorie. Een afgeleide relatie is een resultaatrelatie die is afgeleid van een of meer invoerbasisrelaties, door relationele operatoren uit relationele algebra zoals projectie, intersectie en andere toe te passen op die basisrelaties. Evenzo is in algemene zin een afgeleide tabel in SQL een resultaattabel die is afgeleid van een of meer basistabellen, door expressies te evalueren aan de hand van die invoerbasistabellen.
Even terzijde, ik controleerde hoe de SQL-standaard een basistabel definieert en had meteen spijt dat ik de moeite nam.
4.15.2 Basistabellen
Een basistabel is ofwel een permanente basistabel of een tijdelijke tabel.
Een persistente basistabel is ofwel een gewone persistente basistabel of een systeemversietabel.
Een gewone basistabel is ofwel een gewone vaste basistabel of een tijdelijke tafel."
Hier toegevoegd zonder verdere opmerkingen...
In T-SQL kunt u een basistabel maken met een CREATE TABLE-instructie, maar er zijn andere opties, bijvoorbeeld SELECT INTO en DECLARE @T AS TABLE.
Hier is de standaarddefinitie voor afgeleide tabellen in algemene zin:
4.15.3 Afgeleide tabellen
Een afgeleide tabel is een tabel die direct of indirect is afgeleid van een of meer andere tabellen door de evaluatie van een expressie, zoals een , , of . Een kan een optionele bevatten. De volgorde van de rijen van de tabel gespecificeerd door de is alleen gegarandeerd voor de die onmiddellijk de bevat.”
Er zijn een paar interessante dingen om op te merken over afgeleide tabellen in algemene zin. Men heeft te maken met de opmerking over bestellen. Ik kom hier later in het artikel op terug. Een andere is dat een afgeleide tabel in SQL een geldige stand-alone tabelexpressie kan zijn, maar dat hoeft niet. De volgende uitdrukking vertegenwoordigt bijvoorbeeld een afgeleide tabel, en is wordt ook beschouwd als een geldige stand-alone tabeluitdrukking (u kunt deze uitvoeren):
SELECT custid, companyname
FROM Sales.Customers
WHERE country = N'USA'
Omgekeerd vertegenwoordigt de volgende uitdrukking een afgeleide tabel, maar is niet een geldige zelfstandige tabeluitdrukking:
T1 INNER JOIN T2
ON T1.keycol = T2.keycol
T-SQL ondersteunt een aantal tabeloperators die een afgeleide tabel opleveren, maar die niet worden ondersteund als zelfstandige expressies. Dat zijn:JOIN, PIVOT, UNPIVOT en APPLY. Je hebt wel een clausule nodig om binnen te werken (meestal FROM, maar ook de USING-clausule van de MERGE-instructie) en een hostquery.
Vanaf nu gebruik ik de term afgeleide tabel om een meer specifieke taalconstructie te beschrijven en niet in de algemene zin die hierboven is beschreven.
Syntaxis
Een afgeleide tabel kan worden gedefinieerd als onderdeel van een buitenste SELECT-instructie in de FROM-clausule. Het kan ook worden gedefinieerd als onderdeel van DELETE- en UPDATE-instructies in hun FROM-clausule, en als onderdeel van een MERGE-instructie in de bijbehorende USING-clausule. Ik zal later in dit artikel meer details geven over de syntaxis bij gebruik in wijzigingsverklaringen.
Hier is de syntaxis voor een vereenvoudigde SELECT-query tegen een afgeleide tabel:
SELECT FROM ( ) [ AS ] [ () ] ;
De afgeleide tabeldefinitie verschijnt waar normaal gesproken een basistabel kan verschijnen, in de FROM-component van de buitenste query. Het kan een invoer zijn voor een tabeloperator zoals JOIN, APPLY, PIVOT en UNPIVOT. Wanneer gebruikt als de juiste invoer voor een APPLY-operator, mag het
deel van de afgeleide tabel correlaties hebben met kolommen uit een buitenste tabel (meer hierover in een speciaal toekomstig artikel in de serie). Anders moet de tabeluitdrukking op zichzelf staan.
De buitenste instructie kan alle gebruikelijke query-elementen bevatten. In het geval van een SELECT-instructie:WHERE, GROUP BY, HAVING, ORDER BY en zoals vermeld, tabeloperatoren in de FROM-component.
Hier is een voorbeeld van een eenvoudige zoekopdracht tegen een afgeleide tabel die Amerikaanse klanten vertegenwoordigt:
SELECT custid, companyname
FROM ( SELECT custid, companyname
FROM Sales.Customers
WHERE country = N'USA' ) AS UC;
Deze query genereert de volgende uitvoer:
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
Er zijn drie hoofdonderdelen die moeten worden geïdentificeerd in een verklaring met een afgeleide tabeldefinitie:
De tabeluitdrukking (de innerlijke vraag)
De afgeleide tabelnaam, of beter gezegd, wat in relationele theorie wordt beschouwd als een bereikvariabele
De buitenste verklaring
De tabelexpressie wordt verondersteld een tabel te vertegenwoordigen en moet als zodanig voldoen aan bepaalde vereisten waaraan een normale zoekopdracht niet per se hoeft te voldoen. Ik zal de details binnenkort geven in de sectie "Een tabeluitdrukking is een tabel".
Wat betreft de van het doel afgeleide tabelnaam; een algemene veronderstelling onder T-SQL-ontwikkelaars is dat het slechts een naam of alias is die u aan de doeltabel toewijst. Overweeg ook de volgende vraag:
SELECT custid, companyname
FROM Sales.Customers AS C
WHERE country = N'USA';
Ook hier is de algemene aanname dat AS C slechts een manier is om de tabel Klanten te hernoemen of een alias te noemen voor deze query, te beginnen met de logische stap voor het verwerken van query's waar de naam wordt toegewezen en verder. Vanuit het standpunt van de relationele theorie is er echter een diepere betekenis aan wat C vertegenwoordigt. C is wat bekend staat als een bereikvariabele. C is een afgeleide relatievariabele die zich uitstrekt over de tupels in de invoerrelatievariabele Klanten. In het bovenstaande voorbeeld strekt C zich uit over de tuples in Klanten en evalueert het predikaat land =N'USA'. Tupels waarvoor het predikaat waar evalueert, worden onderdeel van de resultaatrelatie C.
Een tabeluitdrukking is een tabel
Met de achtergrond die ik tot nu toe heb gegeven, zou het geen verrassing moeten zijn wat ik hierna ga uitleggen. Het
deel van een afgeleide tabeldefinitie is een tabel . Dat is zelfs het geval als het wordt uitgedrukt als een vraag. Herinner je je de sluitingseigenschap van relationele algebra nog? Hetzelfde geldt voor de rest van de bovengenoemde benoemde tabeluitdrukkingen (CTE's, views en inline TVF's). Zoals je al hebt geleerd, is de tabel . van SQL is de tegenhanger van de relatie van de relationele theorie , zij het geen perfecte tegenhanger. Een tabelexpressie moet dus aan bepaalde vereisten voldoen om ervoor te zorgen dat het resultaat een tabel is, en dat hoeft niet per se te zijn voor een query die niet als tabelexpressie wordt gebruikt. Hier zijn drie specifieke vereisten: Alle kolommen van de tabelexpressie moeten een naam hebben
Alle kolomnamen van de tabelexpressie moeten uniek zijn
De rijen van de tabelexpressie hebben geen volgorde
Laten we deze vereisten een voor een opsplitsen en de relevantie voor zowel relationele theorie als SQL bespreken.
Alle kolommen moeten een naam hebben
Onthoud dat een relatie een kop en een hoofdtekst heeft. De kop van een relatie is een set attributen (kolommen in SQL). Een attribuut heeft een naam en een typenaam en wordt geïdentificeerd door zijn naam. Een query die niet als tabelexpressie wordt gebruikt, hoeft niet noodzakelijkerwijs namen toe te wijzen aan alle doelkolommen. Beschouw de volgende vraag als voorbeeld:
SELECT empid, firstname, lastname,
CONCAT_WS(N'/', country, region, city)
FROM HR.Employees;
Deze query genereert de volgende uitvoer:
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
De uitvoer van de query heeft een anonieme kolom die het resultaat is van de aaneenschakeling van de locatiekenmerken met behulp van de CONCAT_WS-functie. (Trouwens, deze functie is toegevoegd in SQL Server 2017, dus als je de code in een eerdere versie gebruikt, voel je vrij om deze berekening te vervangen door een alternatieve berekening naar keuze.) Deze query werkt daarom niet een tabel teruggeven, om nog maar te zwijgen van een relatie. Daarom is het niet geldig om een dergelijke query te gebruiken als de tabeluitdrukking/binnenquery-gedeelte van een afgeleide tabeldefinitie.
Probeer het:
SELECT *
FROM ( SELECT empid, firstname, lastname,
CONCAT_WS(N'/', country, region, city)
FROM HR.Employees ) AS D;
U krijgt de volgende foutmelding:
Msg 8155, Level 16, State 2, Line 50 Er is geen kolomnaam opgegeven voor kolom 4 van 'D'.
Even terzijde, merk je iets interessants op aan de foutmelding? Het klaagt over kolom 4 en benadrukt het verschil tussen kolommen in SQL en attributen in relationele theorie.
De oplossing is natuurlijk om ervoor te zorgen dat u expliciet namen toewijst aan kolommen die het resultaat zijn van berekeningen. T-SQL ondersteunt nogal wat kolomnaamgevingstechnieken. Ik zal er twee noemen.
U kunt een inline naamgevingstechniek gebruiken waarbij u de naam van de doelkolom na de berekening en een optionele AS-clausule toewijst, zoals in < expression > [ AS ] < column name >
, zoals zo:
SELECT empid, firstname, lastname, custlocation
FROM ( SELECT empid, firstname, lastname,
CONCAT_WS(N'/', country, region, city) AS custlocation
FROM HR.Employees ) AS D;
Deze query genereert de volgende uitvoer:
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
Met behulp van deze techniek is het heel gemakkelijk om bij het bekijken van de code te zien welke doelkolomnaam aan welke uitdrukking is toegewezen. U hoeft ook alleen kolommen een naam te geven die anders nog geen namen hebben.
U kunt ook een meer externe kolomnaamgevingstechniek gebruiken, waarbij u de doelkolomnamen tussen haakjes direct na de afgeleide tabelnaam specificeert, zoals:
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);
Met deze techniek moet u echter namen voor alle kolommen vermelden, inclusief de kolommen die al namen hebben. De toewijzing van de doelkolomnamen wordt gedaan op positie, van links naar rechts, d.w.z. de eerste doelkolomnaam vertegenwoordigt de eerste uitdrukking in de SELECT-lijst van de inner query; de naam van de tweede doelkolom vertegenwoordigt de tweede uitdrukking; enzovoort.
Merk op dat in het geval van inconsistentie tussen de binnenste en buitenste kolomnamen, bijvoorbeeld als gevolg van een fout in de code, het bereik van de binnennamen de binnenste query is, of, meer precies, de variabele binnenbereik (hier impliciet HR.Employees AS-werknemers) - en het bereik van de buitenste namen is de buitenste bereikvariabele (D in ons geval). Er komt wat meer kijken bij het afbakenen van kolomnamen die te maken hebben met logische queryverwerking, maar dat is een item voor latere discussies.
Het potentieel voor bugs met de externe naamgevingssyntaxis kan het best worden uitgelegd met een voorbeeld.
Bekijk de uitvoer van de vorige query, met de volledige set werknemers uit de tabel HR.Employees. Overweeg dan de volgende vraag en probeer, voordat u deze uitvoert, te achterhalen welke medewerkers u in het resultaat verwacht te zien:
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%';
Als je verwacht dat de query een lege set retourneert voor de gegeven voorbeeldgegevens, aangezien er momenteel geen werknemers zijn met zowel een achternaam als een voornaam die beginnen met de letter D, mis je de bug in de code.
Voer nu de query uit en onderzoek de daadwerkelijke uitvoer:
empid firstname lastname custlocation
------ ---------- --------- ---------------
1 Davis Sara USA/WA/Seattle
9 Doyle Patricia UK/London
Wat is er gebeurd?
De binnenste query specificeert voornaam als de tweede kolom en achternaam als de derde kolom in de SELECT-lijst. De code die de doelkolomnamen van de afgeleide tabel in de buitenste query toewijst, specificeert achternaam als tweede en voornaam als derde. De codenamen voornaam als achternaam en achternaam als voornaam in de bereikvariabele D. In feite filtert u alleen werknemers wiens achternaam begint met de letter D. U filtert niet werknemers met zowel een achternaam als een voornaam die beginnen met de letter D.
De inline aliasing-syntaxis is niet gevoelig voor dergelijke bugs. Ten eerste alias je normaal gesproken geen kolom die al een naam heeft waar je blij mee bent. Ten tweede, zelfs als u een andere alias wilt toewijzen aan een kolom die al een naam heeft, is het niet erg waarschijnlijk dat u met de syntaxis AS de verkeerde alias toewijst. Denk er over na; hoe waarschijnlijk is het dat u zo schrijft:
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%';
Uiteraard niet erg waarschijnlijk.
Alle kolomnamen moeten uniek zijn
Terug naar het feit dat de kop van een relatie een set attributen is, en aangezien een attribuut wordt geïdentificeerd door naam, moeten attribuutnamen uniek zijn voor dezelfde relatie. In een bepaalde query kunt u altijd naar een attribuut verwijzen met een tweedelige naam met de naam van de bereikvariabele als kwalificatie, zoals in .. Als de kolomnaam zonder de kwalificatie ondubbelzinnig is, kunt u het voorvoegsel van de bereikvariabele weglaten. Wat echter belangrijk is om te onthouden, is wat ik eerder zei over de reikwijdte van de kolomnamen. In code die een benoemde tabelexpressie omvat, met zowel een inner query (de tabelexpressie) als een outer query, is het bereik van de kolomnamen in de inner query de variabelen van het binnenbereik, en het bereik van de kolomnamen in de buitenste query zijn de buitenste bereikvariabelen. Als de inner query meerdere brontabellen met dezelfde kolomnaam betreft, kunt u toch op een ondubbelzinnige manier naar die kolommen verwijzen door de bereikvariabelenaam als prefix toe te voegen. Als u de naam van een bereikvariabele niet expliciet toewijst, krijgt u er een impliciet toegewezen, alsof u AS gebruikt.
Beschouw de volgende zelfstandige zoekopdracht als voorbeeld:
SELECT C.custid, O.custid, O.orderid
FROM Sales.Customers AS C
LEFT OUTER JOIN Sales.Orders AS O
ON C.custid = O.custid;
Deze query mislukt niet met een dubbele kolomnaamfout, aangezien de ene custid-kolom feitelijk C.custid heet en de andere O.custid binnen het bereik van de huidige query. Deze query genereert de volgende uitvoer:
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
...
Probeer deze query echter als tabeluitdrukking in de definitie van een afgeleide tabel met de naam CO, zoals:
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;
Wat de buitenste query betreft, hebt u één bereikvariabele met de naam CO, en het bereik van alle kolomnamen in de buitenste query is die bereikvariabele. De namen van alle kolommen in een bepaalde bereikvariabele (onthoud dat een bereikvariabele een relatievariabele is) moeten uniek zijn. Daarom krijgt u de volgende foutmelding:
Msg 8156, Level 16, State 1, Line 80 De kolom 'custid' is meerdere keren opgegeven voor 'CO'.
De oplossing is natuurlijk om verschillende kolomnamen toe te kennen aan de twee custid-kolommen voor zover het de bereikvariabele CO betreft, zoals:
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;
Deze query genereert de volgende uitvoer:
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
...
Als u goede praktijken volgt, vermeldt u expliciet de kolomnamen in de SELECT-lijst van de buitenste zoekopdracht. Omdat er maar één bereikvariabele bij betrokken is, hoeft u de tweedelige naam niet te gebruiken voor de buitenste kolomverwijzingen. Als je de tweedelige naam wilt gebruiken, voeg je de kolomnamen toe aan de buitenste bereikvariabele CO, zoals:
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; Geen bestelling
Ik heb nogal wat te zeggen over benoemde tabeluitdrukkingen en volgorde - genoeg voor een artikel op zich - dus ik zal een toekomstig artikel aan dit onderwerp wijden. Toch wilde ik het onderwerp hier kort aanstippen, omdat het zo belangrijk is. Bedenk dat de hoofdtekst van een relatie een reeks tupels is, en op dezelfde manier is de hoofdtekst van een tabel een reeks rijen. Een set heeft geen volgorde. Toch staat SQL toe dat de buitenste query een ORDER BY-component heeft die een presentatie-ordenende betekenis heeft, zoals de volgende query laat zien:
SELECT orderid, val
FROM Sales.OrderValues
ORDER BY val DESC;
Wat u echter moet begrijpen, is dat deze query als resultaat geen relatie retourneert. Zelfs vanuit het perspectief van SQL, retourneert de query geen tabel als resultaat, en daarom is het niet beschouwd als een tabeluitdrukking. Daarom is het ongeldig om een dergelijke query te gebruiken als het tabelexpressiegedeelte van een afgeleide tabeldefinitie.
Probeer de volgende code uit te voeren:
SELECT orderid, val
FROM ( SELECT orderid, val
FROM Sales.OrderValues
ORDER BY val DESC ) AS D;
U krijgt de volgende foutmelding:
Msg 1033, Level 15, State 1, Line 124 De ORDER BY-clausule is ongeldig in views, inline-functies, afgeleide tabellen, subquery's en algemene tabelexpressies, tenzij TOP, OFFSET of FOR XML ook is opgegeven.
Ik behandel de tenzij binnenkort een deel van de foutmelding.
Als u wilt dat de buitenste query een geordend resultaat retourneert, moet u de ORDER BY-component in de buitenste query opgeven, zoals:
SELECT orderid, val
FROM ( SELECT orderid, val
FROM Sales.OrderValues ) AS D
ORDER BY val DESC;
Wat betreft de tenzij een deel van de foutmelding; T-SQL ondersteunt het gepatenteerde TOP-filter en het standaard OFFSET-FETCH-filter. Beide filters zijn afhankelijk van een ORDER BY-component in hetzelfde querybereik om te definiëren welke bovenste rijen moeten worden gefilterd. Dit is helaas het resultaat van een valkuil in het ontwerp van deze functies, die de volgorde van de presentatie niet scheidt van de volgorde van de filters. Hoe het ook zij, zowel Microsoft met zijn TOP-filter als de standaard met zijn OFFSET-FETCH-filter, staan het specificeren van een ORDER BY-component in de innerlijke query toe, zolang deze ook respectievelijk het TOP- of OFFSET-FETCH-filter specificeert. Deze zoekopdracht is dus geldig, bijvoorbeeld:
SELECT orderid, val
FROM ( SELECT TOP (3) orderid, val
FROM Sales.OrderValues
ORDER BY val DESC ) AS D;
Toen ik deze query op mijn systeem uitvoerde, genereerde het de volgende uitvoer:
orderid val
-------- ---------
10865 16387.50
10981 15810.00
11030 12615.05
Wat echter belangrijk is om te benadrukken, is dat de enige reden dat de ORDER BY-component in de innerlijke query is toegestaan, is om het TOP-filter te ondersteunen. Dat is de enige garantie die je krijgt als het om bestellen gaat. Aangezien de buitenste query ook geen ORDER BY-clausule heeft, krijgt u geen garantie voor een specifieke presentatievolgorde van deze query, ongeacht het waargenomen gedrag. Dat is zowel het geval in T-SQL als in de standaard. Hier is een citaat uit de standaard die dit deel behandelt:
"De volgorde van de rijen van de tabel gespecificeerd door de wordt alleen gegarandeerd voor de die onmiddellijk de bevat."
Zoals eerder vermeld, valt er nog veel meer te zeggen over tabeluitdrukkingen en -ordening, wat ik in een toekomstig artikel zal doen. Ik zal ook voorbeelden geven die aantonen hoe het ontbreken van de ORDER BY-clausule in de buitenste query betekent dat u geen garanties voor het bestellen van presentaties krijgt.
Een tabeluitdrukking, bijvoorbeeld een inner query in een afgeleide tabeldefinitie, is dus een tabel. Evenzo is een afgeleide tabel (in de specifieke zin) zelf ook een tabel. Het is geen basistafel, maar het is toch een tafel. Hetzelfde geldt voor CTE's, views en inline TVF's. Het zijn geen basistabellen, eerder afgeleide tabellen (in de meer algemene zin), maar het zijn niettemin tabellen.
Ontwerpfouten
Afgeleide tabellen hebben twee belangrijke tekortkomingen in hun ontwerp. Beide hebben te maken met het feit dat de afgeleide tabel is gedefinieerd in de FROM-component van de buitenste query.
Een ontwerpfout heeft te maken met het feit dat als je een afgeleide tabel van een buitenste query moet bevragen, en die query op zijn beurt als een tabelexpressie in een andere afgeleide tabeldefinitie moet gebruiken, je uiteindelijk die afgeleide tabelquery's nest. Bij computergebruik leidt het expliciet nesten van code met meerdere niveaus van nesten tot complexe code die moeilijk te onderhouden is.
Hier is een heel eenvoudig voorbeeld dat dit aantoont:
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;
Deze code retourneert besteljaren en het aantal klanten dat elk jaar bestellingen heeft geplaatst, alleen voor jaren waarin het aantal klanten dat bestellingen plaatste groter was dan 70.
De belangrijkste motivatie om hier tabeluitdrukkingen te gebruiken, is om meerdere keren naar een kolomalias te kunnen verwijzen. De binnenste query die als tabelexpressie voor de afgeleide tabel D1 wordt gebruikt, zoekt in de tabel Sales.Orders en wijst de kolomnaam orderyear toe aan de expressie YEAR(orderdate), en retourneert ook de custid-kolom. De query op D1 groepeert de rijen van D1 op orderjaar en retourneert orderjaar evenals het duidelijke aantal klanten dat gedurende het betreffende jaar bestellingen heeft geplaatst, gealiasd als numcusts. De code definieert een afgeleide tabel met de naam D2 op basis van deze query. De buitenste zoekopdracht dan zoekopdrachten D2 en filtert alleen jaren waarin het aantal klanten dat bestellingen plaatste groter was dan 70.
Een poging om deze code te bekijken of problemen op te lossen in geval van problemen is lastig vanwege de meerdere niveaus van nesting. In plaats van de code op de meer natuurlijke manier van boven naar beneden te bekijken, merk je dat je het moet analyseren, beginnend met de binnenste eenheid en geleidelijk naar buiten, omdat dat praktischer is.
Het hele punt over het gebruik van afgeleide tabellen in dit voorbeeld was om de code te vereenvoudigen door de noodzaak om uitdrukkingen te herhalen te vermijden. Maar ik weet niet zeker of deze oplossing dit doel bereikt. In dit geval is het waarschijnlijk beter om sommige uitdrukkingen te herhalen, zodat u geen afgeleide tabellen hoeft te gebruiken, zoals:
SELECT YEAR(orderdate) AS orderyear, COUNT(DISTINCT custid) AS numcusts
FROM Sales.Orders
GROUP BY YEAR(orderdate)
HAVING COUNT(DISTINCT custid) > 70;
Houd er rekening mee dat ik hier een heel eenvoudig voorbeeld laat zien ter illustratie. Stel je productiecode voor met meer nestingniveaus en met langere, meer uitgebreide code, en je kunt zien hoe het aanzienlijk ingewikkelder wordt om te onderhouden.
Een andere fout in het ontwerp van afgeleide tabellen heeft te maken met gevallen waarin u moet communiceren met meerdere instanties van dezelfde afgeleide tabel. Beschouw de volgende vraag als voorbeeld:
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;
Deze code berekent het aantal bestellingen dat in elk jaar wordt verwerkt, evenals het verschil met het voorgaande jaar. Negeer het feit dat er eenvoudigere manieren zijn om dezelfde taak uit te voeren met vensterfuncties - ik gebruik deze code om een bepaald punt te illustreren, dus de taak zelf en de verschillende manieren om het op te lossen zijn niet significant.
Een join is een tabeloperator die zijn twee ingangen als een set behandelt, wat betekent dat er geen volgorde tussen zit. Ze worden de linker- en rechterinvoer genoemd, zodat u een van hen (of beide) kunt markeren als een bewaarde tabel in een outer join, maar toch is er geen eerste en tweede tussen. U mag afgeleide tabellen gebruiken als join-invoer, maar de naam van de bereikvariabele die u toewijst aan de linkerinvoer is niet toegankelijk in de definitie van de rechterinvoer. Dat komt omdat beide conceptueel zijn gedefinieerd in dezelfde logische stap, alsof ze op hetzelfde moment in de tijd zijn. Daarom kunt u bij het samenvoegen van afgeleide tabellen niet twee bereikvariabelen definiëren op basis van één tabeluitdrukking. Helaas moet u de code herhalen en twee bereikvariabelen definiëren op basis van twee identieke kopieën van de code. Dit bemoeilijkt natuurlijk de onderhoudbaarheid van de code en vergroot de kans op bugs. Elke wijziging die u aanbrengt in de ene tabeluitdrukking, moet ook op de andere worden toegepast.
Zoals ik in een toekomstig artikel zal uitleggen, hebben CTE's, in hun ontwerp, niet deze twee gebreken die afgeleide tabellen hebben.
Tabelwaardeconstructor
Met een tabelwaardeconstructor kunt u een tabelwaarde maken op basis van op zichzelf staande scalaire expressies. U kunt dan zo'n tabel in een outer query gebruiken, net zoals u een afgeleide tabel gebruikt die is gebaseerd op een inner query. In een toekomstig artikel bespreek ik lateraal afgeleide tabellen en correlaties in detail, en ik zal meer geavanceerde vormen van tabelwaardeconstructors laten zien. In dit artikel zal ik me echter concentreren op een eenvoudige vorm die puur gebaseerd is op op zichzelf staande scalaire expressies.
The general syntax for a query against a table value constructor is as follows:
SELECT FROM ( ) 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');
Deze code genereert de volgende uitvoer:
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');
Deze code genereert de volgende uitvoer:
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');
Deze code genereert de volgende uitvoer:
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);
Als je klaar bent, voer je de volgende code uit om op te schonen:
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.
Samenvatting
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.