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

Grundlæggende om tabeludtryk, del 5 - CTE'er, logiske overvejelser

Denne artikel er den femte del i en serie om tabeludtryk. I del 1 gav jeg baggrunden for tabeludtryk. I del 2, del 3 og del 4 dækkede jeg både de logiske og optimeringsaspekterne af afledte tabeller. I denne måned starter jeg dækningen af ​​almindelige tabeludtryk (CTE'er). Ligesom med afledte tabeller vil jeg først behandle den logiske behandling af CTE'er, og i fremtiden vil jeg komme til optimeringsovervejelser.

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.

CTE'er

Lad os starte med udtrykket fælles tabeludtryk . Hverken dette udtryk eller dets akronym CTE forekommer i ISO/IEC SQL-standardspecifikationerne. Så det kan være, at begrebet stammer fra et af databaseprodukterne og senere overtaget af nogle af de andre databaseleverandører. Du kan finde det i dokumentationen til Microsoft SQL Server og Azure SQL Database. T-SQL understøtter det fra og med SQL Server 2005. Standarden bruger termen forespørgselsudtryk at repræsentere et udtryk, der definerer en eller flere CTE'er, inklusive den ydre forespørgsel. Den bruger udtrykket med listeelement at repræsentere det, T-SQL kalder en CTE. Jeg giver snart syntaksen for et forespørgselsudtryk.

Bortset fra kilden til udtrykket, almindelig tabeludtryk eller CTE , er det almindeligt anvendte udtryk af T-SQL-udøvere for den struktur, der er fokus i denne artikel. Så lad os først tage fat på, om det er et passende udtryk. Vi har allerede konkluderet, at udtrykket tabeludtryk er passende for et udtryk, der konceptuelt returnerer en tabel. Afledte tabeller, CTE'er, visninger og indlejrede tabelværdier er alle typer navngivne tabeludtryk som T-SQL understøtter. Altså tabeludtrykket del af fælles tabeludtryk virker bestemt passende. Hvad angår det almindelige del af udtrykket, har det sandsynligvis at gøre med en af ​​designfordelene ved CTE'er i forhold til afledte tabeller. Husk, at du ikke kan genbruge det afledte tabelnavn (eller mere præcist områdevariabelnavnet) mere end én gang i den ydre forespørgsel. Omvendt kan CTE-navnet bruges flere gange i den ydre forespørgsel. Med andre ord er CTE-navnet almindeligt til den ydre forespørgsel. Selvfølgelig vil jeg demonstrere dette designaspekt i denne artikel.

CTE'er giver dig lignende fordele som afledte tabeller, herunder muliggør udvikling af modulære løsninger, genbrug af kolonnealiaser, indirekte interaktion med vinduesfunktioner i klausuler, der normalt ikke tillader dem, understøtter modifikationer, der indirekte er afhængige af TOP eller OFFSET FETCH med ordrespecifikation, og andre. Men der er visse designfordele sammenlignet med afledte tabeller, som jeg vil dække i detaljer, når jeg har angivet syntaksen for strukturen.

Syntaks

Her er standardens syntaks for et forespørgselsudtryk:

7.17


Funktion
Angiv en tabel.


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

| UNION [ ALLE | DISTINCT ]
[ ]
| EXCEPT [ ALL | DISTINCT ]
[ ]
::=

| STYR [ ALLE | DISTINCT ]
[ ]
::=

|
[ ] [ ] [ ]

::=
| |
::=TABEL
::=
TILSVARENDE [ AF ]
::=
::=ORDER BY
::=OFFSET { ROW | RÆKKER
::=
FETCH { FIRST | NÆSTE } [ ] { RÆKKE | RÆKKER } { KUN | MED BÅND
::=

|
::=
::=
::= PROCENT


7.18


Funktion
Angiv genereringen af ​​bestillings- og cyklusdetektionsinformation i resultatet af rekursive forespørgselsudtryk.


Format
::=
| |
::=
SØG SET
::=
DYBDE FØRST AF | BREADTH FIRST BY
::=
::=
CYKLUS SET TIL
STANDARD BRUGER
::= [ { }… ]
::=
::=
::=
::=
::=


7.3


Funktion
Angiv et sæt af s, der skal konstrueres til en tabel.


Format
::=VÆRDIER
::=
[ { }… ]
::=
VALUES
::=

[ { }… ]

Standardudtrykket forespørgselsudtryk repræsenterer et udtryk, der involverer en WITH-sætning, en med liste , som er lavet af en eller flere med listeelementer , og en ydre forespørgsel. T-SQL henviser til standarden med listeelement som CTE.

T-SQL understøtter ikke alle standard syntakselementer. For eksempel understøtter den ikke nogle af de mere avancerede rekursive forespørgselselementer, der giver dig mulighed for at kontrollere søgeretningen og håndtere cyklusser i en grafstruktur. Rekursive forespørgsler er i fokus i næste måneds artikel.

Her er T-SQL-syntaksen til en forenklet forespørgsel mod en CTE:

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

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

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

Du vil finde de samme tre dele i en sætning mod en CTE, som du ville finde i en erklæring mod en afledt tabel:

  1. Tabeludtrykket (den indre forespørgsel)
  2. Det navn, der er tildelt tabeludtrykket (navnet på rækkevariablen)
  3. Den ydre forespørgsel

Hvad der er anderledes ved designet af CTE'er sammenlignet med afledte tabeller er, hvor i koden disse tre elementer er placeret. Med afledte tabeller indlejres den indre forespørgsel i den ydre forespørgsels FROM-klausul, og tabeludtrykkets navn tildeles efter selve tabeludtrykket. Elementerne hænger på en måde sammen. Omvendt adskiller koden med CTE'er de tre elementer:først tildeler du tabeludtrykkets navn; for det andet specificerer du tabeludtrykket - fra start til slut uden afbrydelser; for det tredje angiver du den ydre forespørgsel - fra start til slut uden afbrydelser. Senere, under "Designovervejelser", vil jeg forklare implikationerne af disse designforskelle.

Et ord om CTE'er og brugen af ​​et semikolon som en erklæringsterminator. Desværre, i modsætning til standard SQL, tvinger T-SQL dig ikke til at afslutte alle udsagn med et semikolon. Der er dog meget få tilfælde i T-SQL, hvor koden uden en terminator er tvetydig. I de tilfælde er opsigelsen obligatorisk. En sådan sag vedrører det faktum, at WITH-klausulen bruges til flere formål. En er at definere en CTE, en anden er at definere et tabeltip til en forespørgsel, og der er et par ekstra use cases. Som et eksempel bruges WITH-sætningen i følgende sætning til at tvinge det serialiserbare isolationsniveau med et tabeltip:

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

Potentialet for tvetydighed er, når du har en uafsluttet sætning forud for en CTE-definition, i hvilket tilfælde parseren muligvis ikke er i stand til at fortælle, om WITH-sætningen hører til den første eller anden sætning. Her er et eksempel, der viser dette:

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

Her kan parseren ikke fortælle, om WITH-sætningen skal bruges til at definere et tabeltip for Customers-tabellen i den første sætning, eller starte en CTE-definition. Du får følgende fejlmeddelelse:

Msg 336, Level 15, State 1, Line 159
Forkert syntaks nær 'UC'. Hvis dette er beregnet til at være et almindeligt tabeludtryk, skal du eksplicit afslutte den forrige sætning med et semikolon.

Rettelsen er selvfølgelig at afslutte erklæringen forud for CTE-definitionen, men som en bedste praksis bør du virkelig afslutte alle dine erklæringer:

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 har måske bemærket, at nogle mennesker starter deres CTE-definitioner med et semikolon som en praksis, som sådan:

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

Pointen i denne praksis er at reducere potentialet for fremtidige fejl. Hvad hvis nogen på et senere tidspunkt tilføjer en uafsluttet sætning lige før din CTE-definition i scriptet og ikke gider at tjekke hele scriptet, snarere kun deres erklæring? Dit semikolon lige før WITH-klausulen bliver effektivt deres erklæringsafslutning. Du kan helt sikkert se det praktiske i denne praksis, men det er lidt unaturligt. Hvad der anbefales, selvom det er sværere at opnå, er at indgyde god programmeringspraksis i organisationen, herunder opsigelse af alle erklæringer.

Med hensyn til de syntaksregler, der gælder for det tabeludtryk, der bruges som den indre forespørgsel i CTE-definitionen, er de de samme som dem, der gælder for det tabeludtryk, der bruges som den indre forespørgsel i en afledt tabeldefinition. Det er:

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

For detaljer, se afsnittet "Et tabeludtryk er en tabel" i del 2 af serien.

Designovervejelser

Hvis du spørger erfarne T-SQL-udviklere om, hvorvidt de foretrækker at bruge afledte tabeller eller CTE'er, er ikke alle enige om, hvad der er bedst. Naturligvis har forskellige mennesker forskellige stylingpræferencer. Jeg bruger nogle gange afledte tabeller og nogle gange CTE'er. Det er godt at være i stand til bevidst at identificere de specifikke sprogdesignforskelle mellem de to værktøjer og vælge ud fra dine prioriteter i en given løsning. Med tid og erfaring træffer du dine valg mere intuitivt.

Desuden er det vigtigt ikke at forveksle brugen af ​​tabeludtryk og midlertidige tabeller, men det er en præstationsrelateret diskussion, som jeg vil behandle i en fremtidig artikel.

CTE'er har rekursive forespørgselsfunktioner, og afledte tabeller har ikke. Så hvis du har brug for at stole på dem, ville du naturligvis gå med CTE'er. Rekursive forespørgsler er i fokus i næste måneds artikel.

I del 2 forklarede jeg, at jeg ser indlejring af afledte tabeller som at tilføje kompleksitet til koden, da det gør det svært at følge logikken. Jeg gav følgende eksempel, der identificerede ordreår, hvor mere end 70 kunder afgav ordrer:

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 understøtter ikke indlejring. Så når du gennemgår eller fejlfinder en løsning baseret på CTE'er, går du ikke tabt i den indlejrede logik. I stedet for nesting bygger du flere modulære løsninger ved at definere flere CTE'er under den samme WITH-sætning, adskilt af kommaer. Hver af CTE'erne er baseret på en forespørgsel, der er skrevet fra start til slut uden afbrydelser. Jeg ser det som en god ting fra et kodeklarheds- og vedligeholdelsesperspektiv.

Her er en løsning på den førnævnte opgave ved hjælp af CTE'er:

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;

Jeg kan bedre lide den CTE-baserede løsning. Men igen, spørg erfarne udviklere, hvilken af ​​de to ovenstående løsninger de foretrækker, og de vil ikke alle være enige. Nogle foretrækker faktisk den indlejrede logik og at kunne se alt på ét sted.

En meget klar fordel ved CTE'er i forhold til afledte tabeller er, når du skal interagere med flere forekomster af det samme tabeludtryk i din løsning. Husk følgende eksempel baseret på afledte tabeller fra 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;

Denne løsning returnerer ordreår, ordretal pr. år og forskellen mellem indeværende års og det foregående års optællinger. Ja, du kunne gøre det nemmere med LAG-funktionen, men mit fokus her er ikke at finde den bedste måde at nå denne meget specifikke opgave. Jeg bruger dette eksempel til at illustrere visse sprogdesignaspekter af navngivne tabeludtryk.

Problemet med denne løsning er, at du ikke kan tildele et navn til et tabeludtryk og genbruge det i det samme logiske forespørgselsbehandlingstrin. Du navngiver en afledt tabel efter selve tabeludtrykket i FROM-sætningen. Hvis du definerer og navngiver en afledt tabel som det første input i en join, kan du ikke også genbruge det afledte tabelnavn som det andet input af den samme join. Hvis du selv skal forbinde to forekomster af det samme tabeludtryk, har du med afledte tabeller intet andet valg end at duplikere koden. Det er, hvad du gjorde i ovenstående eksempel. Omvendt er CTE-navnet tildelt som det første element i koden blandt de førnævnte tre (CTE-navn, indre forespørgsel, ydre forespørgsel). I logisk forespørgselsbehandling, når du kommer til den ydre forespørgsel, er CTE-navnet allerede defineret og tilgængeligt. Dette betyder, at du kan interagere med flere forekomster af CTE-navnet i den ydre forespørgsel, som sådan:

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;

Denne løsning har en klar programmerbar fordel i forhold til den, der er baseret på afledte tabeller, idet du ikke behøver at vedligeholde to kopier af det samme tabeludtryk. Der er mere at sige om det fra et fysisk behandlingsperspektiv og sammenligne det med brugen af ​​midlertidige tabeller, men det vil jeg gøre i en fremtidig artikel, der fokuserer på ydeevne.

En fordel, som kode baseret på afledte tabeller har sammenlignet med kode baseret på CTE'er, har at gøre med den lukkeegenskab, som et tabeludtryk formodes at have. Husk, at et relationsudtryks lukkeegenskab siger, at både input og output er relationer, og at et relationelt udtryk derfor kan bruges, hvor der forventes en relation, som input til endnu et relationelt udtryk. På samme måde returnerer et tabeludtryk en tabel og formodes at være tilgængelig som en inputtabel for et andet tabeludtryk. Dette gælder for en forespørgsel, der er baseret på afledte tabeller - du kan bruge den, hvor en tabel forventes. For eksempel kan du bruge en forespørgsel, der er baseret på afledte tabeller, som den indre forespørgsel i en CTE-definition, som i følgende eksempel:

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;

Det samme gælder dog ikke for en forespørgsel, der er baseret på CTE'er. Selvom det konceptuelt formodes at blive betragtet som et tabeludtryk, kan du ikke bruge det som den indre forespørgsel i afledte tabeldefinitioner, underforespørgsler og selve CTE'er. For eksempel er følgende kode ikke gyldig 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 gode nyhed er, at du kan bruge en forespørgsel, der er baseret på CTE'er, som den indre forespørgsel i visninger og inline-tabel-vurderede funktioner, som jeg dækker i fremtidige artikler.

Husk også, at du altid kan definere en anden CTE baseret på den sidste forespørgsel, og derefter få den yderste forespørgsel til at interagere 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;

Fra et fejlfindingssynspunkt har jeg som nævnt som regel nemmere ved at følge logikken i kode, der er baseret på CTE'er, sammenlignet med kode baseret på afledte tabeller. Løsninger baseret på afledte tabeller har dog den fordel, at du kan fremhæve ethvert indlejringsniveau og køre det uafhængigt, som vist i figur 1.

Figur 1:Kan fremhæve og køre en del af koden med afledte tabeller

Med CTE'er er tingene sværere. For at kode, der involverer CTE'er, skal kunne køres, skal den starte med et WITH-udtryk efterfulgt af et eller flere navngivne tabeludtryk i parentes adskilt af kommaer, efterfulgt af en forespørgsel uden forudgående komma. Du er i stand til at fremhæve og køre enhver af de indre forespørgsler, der virkelig er selvstændige, såvel som den komplette løsnings kode; du kan dog ikke fremhæve og køre nogen anden mellemliggende del af løsningen. For eksempel viser figur 2 et mislykket forsøg på at køre koden, der repræsenterer C2.

Figur 2:Kan ikke fremhæve og køre en del af koden med CTE'er

Så med CTE'er er du nødt til at ty til noget akavede midler for at kunne fejlfinde et mellemtrin i løsningen. For eksempel er en almindelig løsning midlertidigt at injicere en SELECT * FROM your_cte-forespørgsel lige under den relevante CTE. Du fremhæver og kører derefter koden inklusive den injicerede forespørgsel, og når du er færdig, sletter du den injicerede forespørgsel. Figur 3 viser denne teknik.

Figur 3:Injicer SELECT * under relevant CTE

Problemet er, at når du foretager ændringer i koden – selv midlertidige mindre som ovenstående – er der en chance for, at når du forsøger at vende tilbage til den originale kode, vil du ende med at introducere en ny fejl.

En anden mulighed er at style din kode lidt anderledes, sådan at hver ikke-første CTE-definition starter med en separat kodelinje, der ser sådan ud:

, cte_name AS (

Derefter, når du vil køre en mellemliggende del af koden ned til en given CTE, kan du gøre det med minimale ændringer i din kode. Ved at bruge en linjekommentar kommenterer du kun den ene kodelinje, der svarer til den CTE. Du fremhæver og kører derefter koden ned til og med CTE's indre forespørgsel, som nu betragtes som den yderste forespørgsel, som illustreret i figur 4.

Figur 4:Omarranger syntaks for at muliggøre kommentering af én kodelinje

Hvis du ikke er tilfreds med denne stil, har du endnu en mulighed. Du kan bruge en blokkommentar, der starter lige før kommaet, der går forud for den aktuelle CTE og slutter efter den åbne parentes, som illustreret i figur 5.

Figur 5:Brug blokkommentar

Det bunder i personlige præferencer. Jeg bruger typisk den midlertidigt injicerede SELECT *-forespørgselsteknik.

Tabelværdikonstruktør

Der er en vis begrænsning i T-SQL's understøttelse af tabelværdikonstruktører sammenlignet med standarden. Hvis du ikke er bekendt med konstruktionen, så sørg for at tjekke del 2 i serien først, hvor jeg beskriver det i detaljer. Mens T-SQL giver dig mulighed for at definere en afledt tabel baseret på en tabelværdikonstruktør, tillader den dig ikke at definere en CTE baseret på en tabelværdikonstruktør.

Her er et understøttet eksempel, der bruger en afledt tabel:

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

Desværre er lignende kode, der bruger en CTE, ikke understøttet:

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;

Denne kode genererer følgende fejl:

Msg 156, Level 15, State 1, Line 337
Forkert syntaks nær søgeordet 'VALUES'.

Der er dog et par løsninger. Den ene er at bruge en forespørgsel mod en afledt tabel, der igen er baseret på en tabelværdikonstruktør, som CTE'ens indre forespørgsel, som sådan:

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 anden er at ty til den teknik, som folk brugte, før tabelvurderede konstruktører blev introduceret i T-SQL - ved at bruge en række FROMless-forespørgsler adskilt af UNION ALL-operatorer, som sådan:

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;

Bemærk, at kolonnealiasserne er tildelt lige efter CTE-navnet.

De to metoder bliver algebriseret og optimeret på samme måde, så brug den, du er mere komfortabel med.

Fremstilling af en talfølge

Et værktøj, som jeg bruger ret ofte i mine løsninger, er en hjælpetabel med tal. En mulighed er at oprette en faktisk taltabel i din database og udfylde den med en sekvens af rimelig størrelse. En anden er at udvikle en løsning, der producerer en række tal i farten. For sidstnævnte mulighed vil du have, at inputs skal være afgrænsningerne for det ønskede område (vi kalder dem @low og @high ). Du ønsker, at din løsning understøtter potentielt store rækkevidder. Her er min løsning til dette formål, ved hjælp af CTE'er, med en anmodning om området 1001 til 1010 i dette specifikke eksempel:

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;

Denne kode genererer følgende output:

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

Den første CTE kaldet L0 er baseret på en tabelværdikonstruktør med to rækker. De faktiske værdier der er ubetydelige; det vigtige er, at den har to rækker. Derefter er der en sekvens af fem yderligere CTE'er ved navn L1 til L5, der hver anvender en krydsforbindelse mellem to forekomster af den foregående CTE. Følgende kode beregner antallet af rækker, der potentielt genereres af hver af CTE'erne, hvor @L er CTE-niveaunummeret:

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

Her er de tal, du får for hver CTE:

CTE Kardinalitet
L0 2
L1 4
L2 16
L3 256
L4 65.536
L5 4.294.967.296

At gå op til niveau 5 giver dig over fire milliarder rækker. Dette burde være tilstrækkeligt til enhver praktisk anvendelse, som jeg kan komme i tanke om. Det næste trin finder sted i CTE kaldet Nums. Du bruger en ROW_NUMBER-funktion til at generere en sekvens af heltal, der starter med 1 baseret på ingen defineret rækkefølge (ORDER BY (SELECT NULL)), og navngiver resultatkolonnen rownum. Endelig bruger den ydre forespørgsel et TOP-filter baseret på rækkefølge for at filtrere så mange tal som den ønskede sekvenskardinalitet (@high – @low + 1), og beregner resultattallet n som @low + rownum – 1.

Her kan du virkelig værdsætte skønheden i CTE-designet og de besparelser, det giver, når du bygger løsninger på en modulær måde. I sidste ende udpakker unnesting-processen 32 tabeller, der hver består af to rækker baseret på konstanter. Dette kan tydeligt ses i udførelsesplanen for denne kode, som vist i figur 6 ved brug af SentryOne Plan Explorer.

Figur 6:Plan for forespørgselsgenererende talsekvens

Hver konstant scanningsoperator repræsenterer en tabel med konstanter med to rækker. Sagen er, at den øverste operatør er den, der anmoder om disse rækker, og den kortslutter, efter den har fået det ønskede nummer. Læg mærke til de 10 rækker, der er angivet over pilen, der flyder ind i Top-operatoren.

Jeg ved, at denne artikels fokus er den konceptuelle behandling af CTE'er og ikke fysiske/præstationsmæssige overvejelser, men ved at se på planen kan du virkelig sætte pris på kodens korthed sammenlignet med den langvarige, hvad den oversætter til bag kulisserne.

Ved at bruge afledte tabeller kan du faktisk skrive en løsning, der erstatter hver CTE-reference med den underliggende forespørgsel, som den repræsenterer. Det du får er ret skræmmende:

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.

Oversigt

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. Indsættelse af et Python datetime.datetime-objekt i MySQL

  2. SQL 'AND' eller 'OR' kommer først?

  3. TPC-H-ydelse siden PostgreSQL 8.3

  4. HOUR() Eksempler – MySQL