sql >> Databasteknik >  >> RDS >> Database

Grundläggande tabelluttryck, del 5 – CTE:er, logiska överväganden

Den här artikeln är den femte delen i en serie om tabelluttryck. I del 1 gav jag bakgrunden till tabelluttryck. I del 2, del 3 och del 4 täckte jag både de logiska och optimeringsaspekterna av härledda tabeller. Den här månaden börjar jag bevakningen av vanliga tabelluttryck (CTE). Precis som med härledda tabeller kommer jag först att ta upp den logiska behandlingen av CTE, och i framtiden kommer jag till optimeringsöverväganden.

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

CTE

Låt oss börja med termen vanligt tabelluttryck . Varken denna term eller dess akronym CTE förekommer i ISO/IEC SQL-standardspecifikationerna. Så det kan vara så att termen har sitt ursprung i en av databasprodukterna och senare antagits av några av de andra databasleverantörerna. Du hittar det i dokumentationen för Microsoft SQL Server och Azure SQL Database. T-SQL stöder det från och med SQL Server 2005. Standarden använder termen frågeuttryck för att representera ett uttryck som definierar en eller flera CTE, inklusive den yttre frågan. Den använder termen med listelement att representera vad T-SQL kallar en CTE. Jag kommer att tillhandahålla syntaxen för ett frågeuttryck inom kort.

Källan till termen bortsett från, vanligt tabelluttryck , eller CTE , är den vanligaste termen av T-SQL-utövare för strukturen som är i fokus i denna artikel. Så låt oss först ta upp om det är en lämplig term. Vi har redan kommit fram till att termen tabelluttryck är lämpligt för ett uttryck som begreppsmässigt returnerar en tabell. Härledda tabeller, CTE:er, vyer och inline-tabellvärderade funktioner är alla typer av namngivna tabelluttryck som T-SQL stöder. Så, tabelluttrycket del av vanligt tabelluttryck verkar verkligen lämpligt. När det gäller det vanliga del av termen har det förmodligen att göra med en av designfördelarna med CTE:er jämfört med härledda tabeller. Kom ihåg att du inte kan återanvända det härledda tabellnamnet (eller mer exakt intervallvariabelns namn) mer än en gång i den yttre frågan. Omvänt kan CTE-namnet användas flera gånger i den yttre frågan. Med andra ord är CTE-namnet vanligt till den yttre frågan. Naturligtvis kommer jag att visa denna designaspekt i den här artikeln.

CTE:er ger dig liknande fördelar som härledda tabeller, inklusive möjliggörande av utveckling av modulära lösningar, återanvändning av kolumnalias, indirekt interaktion med fönsterfunktioner i satser som normalt inte tillåter dem, stöder modifieringar som indirekt förlitar sig på TOP eller OFFSET FETCH med orderspecifikation, och andra. Men det finns vissa designfördelar jämfört med härledda tabeller, som jag kommer att täcka i detalj efter att jag tillhandahållit syntaxen för strukturen.

Syntax

Här är standardens syntax för ett frågeuttryck:

7.17


Funktion
Ange en tabell.


Format
::=
[ ]
[ ] [ ] [ ]
::=MED [ REKURSIV ]
::= [ { }… ]
::=
[ ]
AS [ ]
::=
::=

| UNION [ ALLA | DISTINCT ]
[ ]
| EXCEPT [ALLA | DISTINCT ]
[ ]
::=

| INTERSECT [ ALLA | DISTINCT ]
[ ]
::=

|
[ ] [ ] [ ]

::=
| |
::=TABELL
::=
SAMMANDRAG [ AV ]
::=
::=ORDER BY
::=OFFSET { ROW | RADER
::=
HÄMTA { FÖRSTA | NÄSTA } [ ] { RAD | RADER } { ENDAST | MED SLIPS
::=

|
::=
::=
::= PERCENT


7.18


Funktion
Ange genereringen av ordnings- och cykeldetekteringsinformation i resultatet av rekursiva frågeuttryck.


Format
::=
| |
::=
SÖK SET
::=
DEPTH FIRST BY | BREDD FIRST BY
::=
::=
CYKEL SET TILL
STANDARD ANVÄNDA
::= [ { }... ]
::=
::=
::=
::=
::=


7.3


Funktion
Ange en uppsättning s som ska konstrueras till en tabell.


Format
::=VÄRDEN
::=
[ { }… ]
::=
VÄRDEN
::=

[ { }... ]

Standardtermen frågeuttryck representerar ett uttryck som involverar en WITH-sats, en med lista , som är gjord av en eller flera med listelement , och en yttre fråga. T-SQL hänvisar till standarden med listelement som CTE.

T-SQL stöder inte alla standardsyntaxelement. Till exempel stöder den inte några av de mer avancerade rekursiva frågeelementen som låter dig styra sökriktningen och hantera cykler i en grafstruktur. Rekursiva frågor är i fokus i nästa månads artikel.

Här är T-SQL-syntaxen för en förenklad fråga mot en CTE:

WITH < table name > [ (< target columns >) ] AS
(
  < table expression >
)
SELECT < select list >
FROM < table name >;

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

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

Du hittar samma tre delar i ett uttalande mot en CTE som du skulle göra med ett uttalande mot en härledd tabell:

  1. Tabelluttrycket (den inre frågan)
  2. Namnet som tilldelats tabelluttrycket (intervallvariabelns namn)
  3. Den yttre frågan

Det som är annorlunda med designen av CTE jämfört med härledda tabeller är var i koden dessa tre element finns. Med härledda tabeller kapslas den inre frågan i den yttre frågans FROM-sats, och tabelluttryckets namn tilldelas efter själva tabelluttrycket. Elementen är liksom sammanflätade. Omvänt, med CTE:er, separerar koden de tre elementen:först tilldelar du tabelluttrycket namn; för det andra anger du tabelluttrycket – från början till slut utan avbrott; För det tredje anger du den yttre frågan - från början till slut utan avbrott. Senare, under "Designöverväganden", kommer jag att förklara konsekvenserna av dessa designskillnader.

Ett ord om CTE:er och användningen av semikolon som en uttalandeterminator. Tyvärr, till skillnad från standard SQL, tvingar T-SQL dig inte att avsluta alla satser med semikolon. Det finns dock väldigt få fall i T-SQL där utan en terminator koden är tvetydig. I de fallen är uppsägningen obligatorisk. Ett sådant fall gäller det faktum att WITH-klausulen används för flera ändamål. En är att definiera en CTE, en annan är att definiera en tabelltips för en fråga, och det finns några ytterligare användningsfall. Som ett exempel, i följande uttalande används WITH-satsen för att tvinga fram den serialiserbara isoleringsnivån med ett tabelltips:

SELECT custid, country FROM Sales.Customers WITH (SERIALIZABLE);

Potentialen för tvetydighet är när du har en oavslutad sats som föregår en CTE-definition, i vilket fall parsern kanske inte kan avgöra om WITH-satsen tillhör den första eller andra satsen. Här är ett exempel som visar detta:

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

Här kan analysatorn inte avgöra om WITH-satsen ska användas för att definiera en tabelltips för Customers-tabellen i den första satsen, eller starta en CTE-definition. Du får följande felmeddelande:

Msg 336, Level 15, State 1, Line 159
Felaktig syntax nära 'UC'. Om detta är avsett att vara ett vanligt tabelluttryck, måste du uttryckligen avsluta den föregående satsen med ett semikolon.

Lösningen är naturligtvis att avsluta uttalandet som föregår CTE-definitionen, men som en bästa praxis borde du verkligen avsluta alla dina uttalanden:

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

Du kanske har märkt att vissa människor börjar sina CTE-definitioner med semikolon som en praxis, som så:

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

Poängen med denna praxis är att minska risken för framtida fel. Tänk om någon vid ett senare tillfälle lägger till ett oavslutat uttalande precis före din CTE-definition i skriptet och inte bryr sig om att kontrollera hela skriptet, snarare bara deras uttalande? Ditt semikolon precis före WITH-satsen blir i praktiken deras uttalandeterminator. Du kan säkert se det praktiska i denna praxis, men det är lite onaturligt. Vad som rekommenderas, även om det är svårare att uppnå, är att ingjuta goda programmeringsrutiner i organisationen, inklusive uppsägning av alla uttalanden.

När det gäller syntaxreglerna som gäller för tabelluttrycket som används som den inre frågan i CTE-definitionen, är de samma som de som gäller för tabelluttrycket som används som den inre frågan i en härledd tabelldefinition. Dessa är:

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

För detaljer, se avsnittet "Ett tabelluttryck är en tabell" i del 2 av serien.

Designöverväganden

Om du undersöker erfarna T-SQL-utvecklare om de föredrar att använda härledda tabeller eller CTE:er, kommer inte alla att komma överens om vilket som är bäst. Naturligtvis har olika människor olika stylingpreferenser. Jag använder ibland härledda tabeller och ibland CTE:er. Det är bra att medvetet kunna identifiera de specifika skillnaderna i språkdesign mellan de två verktygen och välja utifrån dina prioriteringar i en given lösning. Med tid och erfarenhet gör du dina val mer intuitivt.

Dessutom är det viktigt att inte blanda ihop användningen av tabelluttryck och tillfälliga tabeller, men det är en prestationsrelaterad diskussion som jag kommer att ta upp i en framtida artikel.

CTE:er har rekursiva frågefunktioner och härledda tabeller har inte. Så om du behöver förlita dig på dem, skulle du naturligtvis välja CTE. Rekursiva frågor är i fokus i nästa månads artikel.

I del 2 förklarade jag att jag ser kapsling av härledda tabeller som att lägga till komplexitet till koden, eftersom det gör det svårt att följa logiken. Jag gav följande exempel och identifierade beställningsår under vilka mer än 70 kunder gjorde beställningar:

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

CTE:er stöder inte kapsling. Så när du granskar eller felsöker en lösning baserad på CTE:er går du inte vilse i den kapslade logiken. Istället för att kapsla bygger du fler modulära lösningar genom att definiera flera CTE:er under samma WITH-sats, separerade med kommatecken. Var och en av CTE:erna är baserad på en fråga som skrivs från början till slut utan avbrott. Jag ser det som en bra sak ur ett kodtydlighets- och underhållsperspektiv.

Här är en lösning på ovannämnda uppgift med hjälp av CTE:

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

Jag gillar den CTE-baserade lösningen bättre. Men återigen, fråga erfarna utvecklare vilken av ovanstående två lösningar de föredrar, och de kommer inte alla att hålla med. Vissa föredrar faktiskt den kapslade logiken och att kunna se allt på ett ställe.

En mycket tydlig fördel med CTE:er jämfört med härledda tabeller är när du behöver interagera med flera instanser av samma tabelluttryck i din lösning. Kom ihåg följande exempel baserat på härledda tabeller från del 2 i serien:

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;

Denna lösning returnerar orderår, orderantal per år och skillnaden mellan innevarande års och föregående års antal. Ja, du skulle kunna göra det lättare med LAG-funktionen, men mitt fokus här är inte att hitta det bästa sättet att uppnå denna mycket specifika uppgift. Jag använder det här exemplet för att illustrera vissa språkdesignaspekter av namngivna tabelluttryck.

Problemet med den här lösningen är att du inte kan tilldela ett namn till ett tabelluttryck och återanvända det i samma logiska frågebearbetningssteg. Du namnger en härledd tabell efter själva tabelluttrycket i FROM-satsen. Om du definierar och namnger en härledd tabell som den första ingången i en koppling, kan du inte också återanvända det härledda tabellnamnet som den andra ingången i samma koppling. Om du själv behöver sammanfoga två instanser av samma tabelluttryck, med härledda tabeller har du inget annat val än att duplicera koden. Det är vad du gjorde i exemplet ovan. Omvänt tilldelas CTE-namnet som det första elementet i koden bland de tre ovan (CTE-namn, inre fråga, yttre fråga). I termer av logisk frågebehandling, när du kommer till den yttre frågan, är CTE-namnet redan definierat och tillgängligt. Detta innebär att du kan interagera med flera instanser av CTE-namnet i den yttre frågan, som så:

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

Denna lösning har en klar programmerbarhetsfördel jämfört med den som baseras på härledda tabeller genom att du inte behöver underhålla två kopior av samma tabelluttryck. Det finns mer att säga om det ur ett fysiskt bearbetningsperspektiv, och jämföra det med användningen av tillfälliga tabeller, men jag kommer att göra det i en framtida artikel som fokuserar på prestanda.

En fördel som kod baserad på härledda tabeller har jämfört med kod baserad på CTE:er har att göra med closure-egenskapen som ett tabelluttryck ska ha. Kom ihåg att stängningsegenskapen för ett relationsuttryck säger att både input och output är relationer, och att ett relationsuttryck därför kan användas där en relation förväntas, som input till ännu ett relationsuttryck. På liknande sätt returnerar ett tabelluttryck en tabell och ska vara tillgängligt som en inmatningstabell för ett annat tabelluttryck. Detta gäller för en fråga som är baserad på härledda tabeller – du kan använda den där en tabell förväntas. Du kan till exempel använda en fråga som är baserad på härledda tabeller som den inre frågan i en CTE-definition, som i följande exempel:

WITH C AS
(
  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
)
SELECT orderyear, numcusts
FROM C;

Detsamma gäller dock inte för en fråga som är baserad på CTE. Även om det begreppsmässigt är tänkt att betraktas som ett tabelluttryck, kan du inte använda det som den inre frågan i härledda tabelldefinitioner, underfrågor och själva CTE:er. Till exempel är följande kod inte giltig i T-SQL:

SELECT orderyear, custid
FROM (WITH C1 AS
      (
        SELECT YEAR(orderdate) AS orderyear, custid
        FROM Sales.Orders
      ),
      C2 AS
      (
        SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
        FROM C1
        GROUP BY orderyear
      )
      SELECT orderyear, numcusts
      FROM C2
      WHERE numcusts > 70) AS D;

Den goda nyheten är att du kan använda en fråga som är baserad på CTE:er som den inre frågan i vyer och inline-tabellvärderade funktioner, som jag tar upp i framtida artiklar.

Kom också ihåg att du alltid kan definiera en annan CTE baserat på den senaste frågan och sedan låta den yttersta frågan interagera med den CTE:

WITH C1 AS
(
  SELECT YEAR(orderdate) AS orderyear, custid
  FROM Sales.Orders
),
C2 AS
(
  SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
  FROM C1
  GROUP BY orderyear
),
C3 AS
(
  SELECT orderyear, numcusts
  FROM C2
  WHERE numcusts &gt; 70
)
SELECT orderyear, numcusts
FROM C3;

Ur felsökningssynpunkt, som nämnt, har jag vanligtvis lättare att följa logiken för kod som är baserad på CTE, jämfört med kod baserad på härledda tabeller. Lösningar baserade på härledda tabeller har dock en fördel genom att du kan markera vilken kapslingsnivå som helst och köra den självständigt, som visas i figur 1.

Figur 1:Kan markera och köra en del av koden med härledda tabeller

Med CTE är saker och ting svårare. För att kod som involverar CTE:er ska vara körbar måste den börja med en WITH-sats, följt av ett eller flera namngivna tabelluttryck i parentes separerade med kommatecken, följt av en fråga utan föregående kommatecken. Du kan markera och köra vilken som helst av de inre frågorna som verkligen är fristående, såväl som den kompletta lösningens kod; Du kan dock inte markera och köra någon annan mellanliggande del av lösningen. Till exempel visar figur 2 ett misslyckat försök att köra koden som representerar C2.

Figur 2:Kan inte markera och köra en del av koden med CTE

Så med CTE:er måste du ta till något besvärliga medel för att kunna felsöka ett mellansteg i lösningen. Till exempel är en vanlig lösning att tillfälligt injicera en SELECT * FROM your_cte-fråga precis under den relevanta CTE. Du markerar och kör sedan koden inklusive den injicerade frågan, och när du är klar tar du bort den injicerade frågan. Figur 3 visar denna teknik.

Figur 3:Injicera SELECT * under relevant CTE

Problemet är att när du gör ändringar i koden – även tillfälliga mindre sådana som ovan – finns det en chans att när du försöker återgå till den ursprungliga koden kommer du att introducera en ny bugg.

Ett annat alternativ är att utforma din kod lite annorlunda, så att varje icke-första CTE-definition börjar med en separat kodrad som ser ut så här:

, cte_name AS (

Sedan, närhelst du vill köra en mellanliggande del av koden ner till en given CTE, kan du göra det med minimala ändringar av din kod. Genom att använda en radkommentar kommenterar du bara den kodraden som motsvarar den CTE. Du markerar sedan och kör koden ner till och med CTE:s inre fråga, som nu anses vara den yttersta frågan, som illustreras i figur 4.

Figur 4:Ordna om syntax för att möjliggöra kommentering av en kodrad

Om du inte är nöjd med den här stilen har du ännu ett alternativ. Du kan använda en blockkommentar som börjar precis före kommatecken som föregår CTE av intresse och slutar efter den öppna parentesen, som illustreras i figur 5.

Figur 5:Använd blockkommentar

Det handlar om personliga preferenser. Jag använder vanligtvis den tillfälligt injicerade SELECT * frågetekniken.

Tabellvärdekonstruktor

Det finns en viss begränsning i T-SQL:s stöd för tabellvärdekonstruktörer jämfört med standarden. Om du inte är bekant med konstruktionen, se till att kolla in del 2 i serien först, där jag beskriver den i detalj. Medan T-SQL låter dig definiera en härledd tabell baserad på en tabellvärdekonstruktor, tillåter den dig inte att definiera en CTE baserad på en tabellvärdeskonstruktor.

Här är ett exempel som stöds som använder en härledd tabell:

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

Tyvärr stöds inte liknande kod som använder en CTE:

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

Den här koden genererar följande fel:

Msg 156, Level 15, State 1, Line 337
Felaktig syntax nära nyckelordet 'VALUES'.

Det finns dock ett par lösningar. En är att använda en fråga mot en härledd tabell, som i sin tur är baserad på en tabellvärdekonstruktor, som CTE:s inre fråga, som så:

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

En annan är att tillgripa tekniken som människor använde innan tabellvärderade konstruktörer introducerades i T-SQL – genom att använda en serie FROMless-frågor separerade av UNION ALL-operatorer, som så:

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

Observera att kolumnaliasen tilldelas direkt efter CTE-namnet.

De två metoderna blir algebriserade och optimerade på samma sätt, så använd den du är mer bekväm med.

Ta fram en talföljd

Ett verktyg som jag använder ganska ofta i mina lösningar är en hjälptabell med tal. Ett alternativ är att skapa en tabell med faktiska tal i din databas och fylla i den med en lagom stor sekvens. En annan är att utveckla en lösning som producerar en talföljd i farten. För det senare alternativet vill du att ingångarna ska vara avgränsare för det önskade intervallet (vi kallar dem @low och @high ). Du vill att din lösning ska stödja potentiellt stora räckvidder. Här är min lösning för detta ändamål, med hjälp av CTE:er, med en begäran om intervallet 1001 till 1010 i detta specifika exempel:

DECLARE @low AS BIGINT = 1001, @high AS BIGINT = 1010;
 
WITH
  L0 AS ( SELECT 1 AS c FROM (VALUES(1),(1)) AS D(c) ),
  L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
  L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
  L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
  L4 AS ( SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B ),
  L5 AS ( SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B ),
  Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
            FROM L5 )
SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
FROM Nums
ORDER BY rownum;

Denna kod genererar följande utdata:

n
-----
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010

Den första CTE som kallas L0 är baserad på en tabellvärdekonstruktor med två rader. De faktiska värdena där är obetydliga; Det viktiga är att den har två rader. Sedan finns det en sekvens av fem ytterligare CTE:er som heter L1 till L5, som var och en tillämpar en korskoppling mellan två instanser av föregående CTE. Följande kod beräknar antalet rader som potentiellt genereras av var och en av CTE:erna, där @L är CTE-nivånumret:

DECLARE @L AS INT = 5;
 
SELECT POWER(2., POWER(2., @L));

Här är siffrorna som du får för varje CTE:

CTE Kardinalitet
L0 2
L1 4
L2 16
L3 256
L4 65 536
L5 4 294 967 296

Att gå upp till nivå 5 ger dig över fyra miljarder rader. Detta borde vara tillräckligt för alla praktiska användningsfall som jag kan tänka mig. Nästa steg sker i CTE som kallas Nums. Du använder en ROW_NUMBER-funktion för att generera en sekvens av heltal som börjar med 1 baserat på ingen definierad ordning (ORDER BY (SELECT NULL)), och namnger resultatkolumnen rownum. Slutligen använder den yttre frågan ett TOP-filter baserat på rownum-ordning för att filtrera så många siffror som önskad sekvenskardinalitet (@high – @low + 1), och beräknar resultatnumret n som @low + rownum – 1.

Här kan du verkligen uppskatta skönheten i CTE-designen och de besparingar som den möjliggör när du bygger lösningar på ett modulärt sätt. I slutändan packar avvecklingsprocessen upp 32 tabeller, som var och en består av två rader baserade på konstanter. Detta kan tydligt ses i exekveringsplanen för denna kod, som visas i figur 6 med SentryOne Plan Explorer.

Figur 6:Plan för frågegenererande nummersekvens

Varje konstant avsökningsoperator representerar en tabell med konstanter med två rader. Saken är den att Top-operatören är den som begär dessa rader, och den kortsluter efter att den fått önskat nummer. Lägg märke till de 10 raderna ovanför pilen som flödar in i Top-operatorn.

Jag vet att den här artikelns fokus är den konceptuella behandlingen av CTE och inte fysiska/prestandaöverväganden, men genom att titta på planen kan du verkligen uppskatta hur kortfattad koden är jämfört med den långrandiga vad den översätter till bakom kulisserna.

Med hjälp av härledda tabeller kan du faktiskt skriva en lösning som ersätter varje CTE-referens med den underliggande frågan som den representerar. Det du får är ganska läskigt:

DECLARE @low AS BIGINT = 1001, @high AS BIGINT = 1010;
 
SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
FROM ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
       FROM ( SELECT 1 AS C
              FROM ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
 
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D7
                CROSS JOIN
                   ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D8 ) AS D9
         CROSS JOIN
            ( SELECT 1 AS C
              FROM ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D7
                CROSS JOIN
                   ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D8 ) AS D10 ) AS Nums
ORDER BY rownum;

Obviously, you don’t want to write a solution like this, but it’s a good way to illustrate what SQL Server does behind the scenes with your CTE code.

If you were really planning to write a solution based on derived tables, instead of using the above nested approach, you’d be better off simplifying the logic to a single query with 31 cross joins between 32 table value constructors, each based on two rows, like so:

DECLARE @low AS BIGINT = 1001, @high AS BIGINT = 1010;
 
SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
FROM ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
       FROM         (VALUES(1),(1)) AS D01(c)
         CROSS JOIN (VALUES(1),(1)) AS D02(c)
         CROSS JOIN (VALUES(1),(1)) AS D03(c)
         CROSS JOIN (VALUES(1),(1)) AS D04(c)
         CROSS JOIN (VALUES(1),(1)) AS D05(c)
         CROSS JOIN (VALUES(1),(1)) AS D06(c)
         CROSS JOIN (VALUES(1),(1)) AS D07(c)
         CROSS JOIN (VALUES(1),(1)) AS D08(c)
         CROSS JOIN (VALUES(1),(1)) AS D09(c)
         CROSS JOIN (VALUES(1),(1)) AS D10(c)
         CROSS JOIN (VALUES(1),(1)) AS D11(c)
         CROSS JOIN (VALUES(1),(1)) AS D12(c)
         CROSS JOIN (VALUES(1),(1)) AS D13(c)
         CROSS JOIN (VALUES(1),(1)) AS D14(c)
         CROSS JOIN (VALUES(1),(1)) AS D15(c)
         CROSS JOIN (VALUES(1),(1)) AS D16(c)
         CROSS JOIN (VALUES(1),(1)) AS D17(c)
         CROSS JOIN (VALUES(1),(1)) AS D18(c)
         CROSS JOIN (VALUES(1),(1)) AS D19(c)
         CROSS JOIN (VALUES(1),(1)) AS D20(c)
         CROSS JOIN (VALUES(1),(1)) AS D21(c)
         CROSS JOIN (VALUES(1),(1)) AS D22(c)
         CROSS JOIN (VALUES(1),(1)) AS D23(c)
         CROSS JOIN (VALUES(1),(1)) AS D24(c)
         CROSS JOIN (VALUES(1),(1)) AS D25(c)
         CROSS JOIN (VALUES(1),(1)) AS D26(c)
         CROSS JOIN (VALUES(1),(1)) AS D27(c)
         CROSS JOIN (VALUES(1),(1)) AS D28(c)
         CROSS JOIN (VALUES(1),(1)) AS D29(c)
         CROSS JOIN (VALUES(1),(1)) AS D30(c)
         CROSS JOIN (VALUES(1),(1)) AS D31(c)
         CROSS JOIN (VALUES(1),(1)) AS D32(c) ) AS Nums
ORDER BY rownum;

Still, the solution based on CTEs is obviously significantly simpler. The plans are identical.

Used in modification statements

CTEs can be used as the source and target tables in INSERT, UPDATE, DELETE and MERGE statements. They cannot be used in the TRUNCATE statement.

The syntax is pretty straightforward. You start the statement as usual with a WITH clause, followed by one or more CTEs separated by commas. Then you specify the outer modification statement, which interacts with the CTEs that were defined under the WITH clause as the source tables, target table, or both. Just like I explained in Part 2 about derived tables, also with CTEs what really gets modified is the underlying base table that the table expression uses. I’ll show a couple of examples using DELETE and UPDATE statements, but remember that you can use CTEs in MERGE and INSERT statements as well.

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

WITH < table name > [ (< target columns >) ] AS
(
  < table expression >
)
DELETE [ FROM ] <table name>
[ WHERE <filter predicate> ];

As an example (don’t actually run it), the following code deletes the 10 oldest orders:

WITH OldestOrders AS
(
  SELECT TOP (10) *
  FROM Sales.Orders
  ORDER BY orderdate, orderid
)
DELETE FROM OldestOrders;

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

WITH < table name > [ (< target columns >) ] AS
(
  < table expression >
)
UPDATE <table name>
  SET <assignments>
[ WHERE <filter predicate> ];

As an example, the following code updates the 10 oldest unshipped orders that have an overdue required date, increasing the required date to 10 days from today:

BEGIN TRAN;
 
WITH OldestUnshippedOrders AS
(
  SELECT TOP (10) orderid, requireddate,
    DATEADD(day, 10, CAST(SYSDATETIME() AS DATE)) AS newrequireddate
  FROM Sales.Orders
  WHERE shippeddate IS NULL
    AND requireddate &lt; CAST(SYSDATETIME() AS DATE)
  ORDER BY orderdate, orderid
)
UPDATE OldestUnshippedOrders
  SET requireddate = newrequireddate
    OUTPUT
      inserted.orderid,
      deleted.requireddate AS oldrequireddate,
      inserted.requireddate AS newrequireddate;
 
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 required dates:

orderid     oldrequireddate newrequireddate
----------- --------------- ---------------
11008       2019-05-06      2020-07-16
11019       2019-05-11      2020-07-16
11039       2019-05-19      2020-07-16
11040       2019-05-20      2020-07-16
11045       2019-05-21      2020-07-16
11051       2019-05-25      2020-07-16
11054       2019-05-26      2020-07-16
11058       2019-05-27      2020-07-16
11059       2019-06-10      2020-07-16
11061       2019-06-11      2020-07-16

(10 rows affected)

Of course you will get a different new required date based on when you run this code.

Summary

I like CTEs. They have a few advantages compared to derived tables. Instead of nesting the code, you define multiple CTEs separated by commas, typically leading to a more modular solution that is easier to review and maintain. Also, you can have multiple references to the same CTE name in the outer statement, so you don’t need to repeat the inner table expression’s code. However, unlike derived tables, CTEs cannot be defined directly based on a table value constructor, and you cannot highlight and execute some of the intermediate parts of the code. The following table summarizes the differences between derived tables and CTEs:

Item Derived table CTE
Supports nesting Yes No
Supports multiple references No Yes
Supports table value constructor Yes No
Can highlight and run part of code Yes No
Supports recursion No Yes

As the last item says, derived tables do not support recursive capabilities, whereas CTEs do. Recursive queries are the focus of next month’s article.


  1. Automatiserad testning av uppgraderingsprocessen för PXC/MariaDB Galera Cluster

  2. Är det möjligt att mata ut en SELECT-sats från ett PL/SQL-block?

  3. Generera datum mellan datumintervall

  4. Beräkna tidsskillnaden mellan två rader