Organiser spørringer med felles tabelluttrykk

Fullført

Når du jobber med komplekse spørringer, må du ofte dele opp logikken i håndterbare deler eller referere til samme delspørring flere ganger. Common Table Expressions (CTEs) gir en måte å definere midlertidige navngitte resultatsett som kun eksisterer under én enkelt spørring, noe som gjør koden din mer lesbar og vedlikeholdbar.

Forstå CTE-syntaks

Et felles tabelluttrykk defineres ved hjelp av klausulen WITH , etterfulgt av CTE-navnet, en valgfri kolonneliste og en spørring som definerer resultatsettet. CTE-en kan deretter refereres til i den påfølgende SELECT, INSERT, UPDATE, eller DELETE setningen.

WITH CTE_Name (Column1, Column2)
AS
(
    -- CTE query definition
    SELECT Column1, Column2
    FROM SomeTable
    WHERE SomeCondition = 'Value'
)
SELECT * FROM CTE_Name;

CTE-er tilbyr flere fordeler sammenlignet med avledede tabeller og underforespørsler:

  • Forbedret lesbarhet: Komplekse spørringer blir lettere å forstå når de deles inn i navngitte logiske seksjoner
  • Selvrefererende evne: Rekursive CTE-er kan referere til seg selv, noe som muliggjør hierarkisk datagjennomgang
  • Flere referanser: Referer til samme CTE flere ganger i den ytre spørringen uten å omdefinere den
  • Modulært design: Bygg komplekse spørringer inkrementelt ved å definere flere CTE-er

Bemerkning

CTE-er er midlertidige resultatsett som kun eksisterer under spørringsutførelse. I motsetning til tradisjonelle tabeller, varer de ikke lenger enn uttalelsen som bruker dem, og krever ikke eksplisitt opprydding.

Lag ikke-rekursive CTE-er

Ikke-rekursive CTE-er definerer et resultatsett basert på en enkel spørring som ikke refererer til seg selv. Dette mønsteret er nyttig for å forenkle komplekse joins, bryte ned flertrinnsberegninger eller forbedre kodeorganisering.

Følgende eksempel bruker en CTE for å beregne salgsmålinger før den kombineres med produktinformasjon:

WITH SalesSummary AS
(
    SELECT 
        ProductID,
        SUM(OrderQty) AS TotalQuantity,
        SUM(LineTotal) AS TotalRevenue,
        COUNT(DISTINCT SalesOrderID) AS OrderCount
    FROM SalesLT.SalesOrderDetail
    GROUP BY ProductID
)
SELECT 
    p.Name AS ProductName,
    p.ProductNumber,
    p.ListPrice,
    ss.TotalQuantity,
    ss.TotalRevenue,
    ss.OrderCount,
    ss.TotalRevenue / NULLIF(ss.TotalQuantity, 0) AS AverageUnitPrice
FROM SalesLT.Product AS p
INNER JOIN SalesSummary AS ss
    ON p.ProductID = ss.ProductID
ORDER BY ss.TotalRevenue DESC;

Denne spørringen oppretter først en CTE kalt SalesSummary som aggregerer ordredetaljer etter produkt, og beregner total solgt mengde, total inntekt og ordreantall. Hovedspørringen kobler deretter denne CTE-en til tabellen Product for å vise produktnavn sammen med deres salgsmålinger. Funksjonen NULLIF forhindrer divisjon med null når gjennomsnittlig enhetspris beregnes.

Du kan definere flere CTE-er i én WITH setning ved å skille dem med kommaer. Senere CTE-er kan referere til tidligere data, noe som muliggjør progressiv datatransformasjon:

WITH CategorySales AS
(
    SELECT 
        p.ProductCategoryID,
        SUM(sod.LineTotal) AS CategoryRevenue
    FROM SalesLT.Product AS p
    INNER JOIN SalesLT.SalesOrderDetail AS sod
        ON p.ProductID = sod.ProductID
    GROUP BY p.ProductCategoryID
),
RankedCategories AS
(
    SELECT 
        ProductCategoryID,
        CategoryRevenue,
        RANK() OVER (ORDER BY CategoryRevenue DESC) AS RevenueRank
    FROM CategorySales
)
SELECT 
    pc.Name AS CategoryName,
    rc.CategoryRevenue,
    rc.RevenueRank
FROM RankedCategories AS rc
INNER JOIN SalesLT.ProductCategory AS pc
    ON rc.ProductCategoryID = pc.ProductCategoryID
WHERE rc.RevenueRank <= 5;

Tips

Når du bygger spørringer med flere CTE-er, start med de mest detaljerte datatransformasjonene og aggreger eller filtrer gradvis i påfølgende CTE-er. Denne tilnærmingen gjør feilsøking enklere siden du kan teste hver CTE uavhengig.

Lag rekursive CTE-er

Rekursive CTE-er refererer til seg selv for å behandle hierarkiske eller graflignende datastrukturer. En rekursiv CTE består av to deler: et ankermedlem som gir det opprinnelige resultatsettet, og et rekursivt medlem som refererer til CTE for å bygge videre på tidligere resultater.

Den generelle syntaksen for en rekursiv CTE er:

WITH RecursiveCTE AS
(
    -- Anchor member: starting point
    SELECT columns
    FROM table
    WHERE starting_condition
    
    UNION ALL
    
    -- Recursive member: references the CTE
    SELECT columns
    FROM table
    INNER JOIN RecursiveCTE
        ON join_condition
)
SELECT * FROM RecursiveCTE;

Et vanlig bruksområde er å navigere i et organisatorisk hierarki. Tenk deg en ansatttabell hvor hver ansatt har en leder:

WITH EmployeeHierarchy AS
(
    -- Anchor: Start with top-level managers (no manager)
    SELECT 
        EmployeeID,
        FirstName,
        LastName,
        ManagerID,
        0 AS Level,
        CAST(FirstName + ' ' + LastName AS NVARCHAR(500)) AS HierarchyPath
    FROM HumanResources.Employee
    WHERE ManagerID IS NULL
    
    UNION ALL
    
    -- Recursive: Find employees who report to previously found employees
    SELECT 
        e.EmployeeID,
        e.FirstName,
        e.LastName,
        e.ManagerID,
        eh.Level + 1,
        CAST(eh.HierarchyPath + ' > ' + e.FirstName + ' ' + e.LastName AS NVARCHAR(500))
    FROM HumanResources.Employee AS e
    INNER JOIN EmployeeHierarchy AS eh
        ON e.ManagerID = eh.EmployeeID
)
SELECT 
    EmployeeID,
    FirstName,
    LastName,
    Level,
    HierarchyPath
FROM EmployeeHierarchy
ORDER BY HierarchyPath;

Viktig!

Rekursive CTE-er kan forårsake uendelige løkker hvis termineringsbetingelsen aldri oppfylles. SQL Server begrenser rekursjon til 100 nivåer som standard. Bruk OPTION (MAXRECURSION n) for å endre denne grensen, hvor n er maksimal rekursjonsdybde (0 for ubegrenset).

Generer sekvenser med rekursive CTE-er

Rekursive CTE-er utmerker seg i å generere sekvenser av tall eller datoer uten å kreve en fysisk talltabell. Denne teknikken er nyttig for å lage datointervaller, fylle hull i data eller generere testdata:

-- Generate a sequence of dates for the current month
WITH DateSequence AS
(
    -- Anchor: Get the first day of the current month
    SELECT CAST(DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0) AS DATE) AS DateValue
    
    UNION ALL
    
    -- Recursive: Add one day until we reach the end of the month
    SELECT DATEADD(DAY, 1, DateValue)
    FROM DateSequence
    WHERE DateValue < EOMONTH(GETDATE())
)
-- Output each date with its day name
SELECT DateValue, DATENAME(WEEKDAY, DateValue) AS DayName
FROM DateSequence
OPTION (MAXRECURSION 31);  -- Allow up to 31 iterations (max days in a month)

Du kan kombinere genererte sekvenser med faktiske data for å identifisere hull eller lage sammendragsrapporter:

-- Generate a numbers table from 1 to 1000
WITH Numbers AS
(
    -- Anchor: Start with 1
    SELECT 1 AS n
    UNION ALL
    -- Recursive: Increment until we reach 1000
    SELECT n + 1 FROM Numbers WHERE n < 1000
),
-- Convert numbers to dates for the entire year
DateRange AS
(
    SELECT DATEADD(DAY, n - 1, '2024-01-01') AS OrderDate
    FROM Numbers
    WHERE DATEADD(DAY, n - 1, '2024-01-01') <= '2024-12-31'
)
-- Count orders for each date, showing 0 for dates with no orders
SELECT 
    dr.OrderDate,
    COALESCE(COUNT(soh.SalesOrderID), 0) AS OrderCount
FROM DateRange AS dr
LEFT JOIN SalesLT.SalesOrderHeader AS soh
    ON CAST(soh.OrderDate AS DATE) = dr.OrderDate
GROUP BY dr.OrderDate
ORDER BY dr.OrderDate
OPTION (MAXRECURSION 366);  -- Allow up to 366 iterations (leap year)

Bruk CTE-er med dataendringssetninger

CTE-er kan brukes med INSERT, , og DELETE setninger, ikke bare SELECTUPDATE. Denne funksjonaliteten er nyttig når modifikasjonslogikken krever kompleks filtrering eller beregninger:

-- Update using a CTE to identify target rows
WITH DiscontinuedProducts AS
(
    SELECT ProductID
    FROM SalesLT.Product
    WHERE SellEndDate < DATEADD(YEAR, -2, GETDATE())
        AND ProductID NOT IN (
            SELECT DISTINCT ProductID 
            FROM SalesLT.SalesOrderDetail
            WHERE ModifiedDate > DATEADD(YEAR, -1, GETDATE())
        )
)
UPDATE SalesLT.Product
SET DiscontinuedDate = GETDATE()
WHERE ProductID IN (SELECT ProductID FROM DiscontinuedProducts);

Denne spørringen bruker en CTE for å identifisere produkter som skal merkes som avviklet. CTE finner produkter hvor sluttdatoen for salg er mer enn to år siden og som ikke har dukket opp i noen ordredetaljer som er endret det siste året. Setningen UPDATE setter deretter for DiscontinuedDate disse produktene. Ved å skille ut utvalgslogikken i en CTE, blir spørringen lettere å lese og teste uavhengig.

For mer informasjon om Common Table Expressions, se WITH common_table_expression (Transact-SQL) og Rekursive spørringer som bruker Common Table Expressions.