Common table expressions (WITH)
Name a query and use it like a table: WITH syntax, chaining several CTEs step by step, using one CTE twice, the semicolon rule, scope, and how CTEs compare with derived tables, temp tables and views.
WITH OrderTotals AS ( SELECT OrderId, SUM(Quantity * UnitPrice) AS Total FROM dbo.OrderItems GROUP BY OrderId)SELECT o.OrderId, c.FirstName, t.TotalFROM OrderTotals AS tJOIN dbo.Orders AS o ON o.OrderId = t.OrderIdJOIN dbo.Customers AS c ON c.CustomerId = o.CustomerIdWHERE t.Total > 100ORDER BY t.Total DESC;OrderId FirstName Total
------- --------- ------
4 Dev 524.00
7 Dev 199.50
5 Farid 120.50
2 Ben 113.00
3 Asha 112.80
8 Zoë 102.50Several CTEs, one step at a time
WITH OrderTotals AS ( SELECT OrderId, SUM(Quantity * UnitPrice) AS Total FROM dbo.OrderItems GROUP BY OrderId),CustomerTotals AS ( SELECT o.CustomerId, COUNT(*) AS Orders, SUM(t.Total) AS Spent FROM OrderTotals AS t JOIN dbo.Orders AS o ON o.OrderId = t.OrderId WHERE o.Status <> 'Cancelled' GROUP BY o.CustomerId),Average AS ( SELECT AVG(Spent) AS AvgSpent FROM CustomerTotals)SELECT c.FirstName, ct.Orders, ct.Spent, a.AvgSpentFROM CustomerTotals AS ctJOIN dbo.Customers AS c ON c.CustomerId = ct.CustomerIdCROSS JOIN Average AS aWHERE ct.Spent > a.AvgSpentORDER BY ct.Spent DESC;FirstName Orders Spent AvgSpent --------- ------ ------ ---------- Dev 2 723.50 263.930000
Naming the columns
WITH MonthlyOrders (Month, Orders) AS ( SELECT DATEFROMPARTS(YEAR(OrderDate), MONTH(OrderDate), 1), COUNT(*) FROM dbo.Orders GROUP BY DATEFROMPARTS(YEAR(OrderDate), MONTH(OrderDate), 1))SELECT Month, OrdersFROM MonthlyOrdersWHERE Orders >= 2ORDER BY Month;Month Orders ---------- ------ 2024-02-01 2 2024-03-01 2 2024-04-01 2 2024-05-01 2 2024-06-01 2
Using a CTE twice
WITH Monthly AS ( SELECT MONTH(o.OrderDate) AS M, SUM(oi.Quantity * oi.UnitPrice) AS Revenue FROM dbo.Orders AS o JOIN dbo.OrderItems AS oi ON oi.OrderId = o.OrderId GROUP BY MONTH(o.OrderDate))SELECT cur.M AS Month, cur.Revenue, prev.Revenue AS PrevMonth, cur.Revenue - prev.Revenue AS ChangeFROM Monthly AS curLEFT JOIN Monthly AS prev ON prev.M = cur.M - 1ORDER BY cur.M;Month Revenue PrevMonth Change
----- ------- --------- -------
2 151.00 NULL NULL
3 636.80 151.00 485.80
4 134.10 636.80 -502.70
5 302.00 134.10 167.90
6 109.35 302.00 -192.65The semicolon rule
WITH has other meanings in T-SQL (such as table hints), so the statement before a CTE must end with a semicolon:
SELECT COUNT(*) AS Orders FROM dbo.OrdersWITH Recent AS (SELECT * FROM dbo.Orders WHERE OrderDate >= '2024-06-01')SELECT OrderId FROM Recent;Msg 336, Level 15, State 1, Line 2 Incorrect syntax near 'Recent'. If this is intended to be a common table expression, you need to explicitly terminate the previous statement with a semi-colon.
;WITH. Ending every statement with a semicolon, as this course does, avoids the problem.
A CTE lives for one statement
WITH Pending AS (SELECT OrderId FROM dbo.Orders WHERE Status = 'Pending')SELECT COUNT(*) AS PendingOrders FROM Pending; SELECT * FROM Pending;PendingOrders
-------------
2
Msg 208, Level 16, State 1, Line 4
Invalid object name 'Pending'.Changing data through a CTE
WITH OutOfStock AS ( SELECT ProductId, Name, Stock FROM dbo.Products WHERE Stock = 0)UPDATE OutOfStock SET Stock = 25; SELECT Name, Stock FROM dbo.Products WHERE ProductId IN (9, 12);Name Stock ---------------- ----- Wall plugs (100) 25 Safety goggles 25
CTE, derived table, temp table or view?
| Lives for | Reusable | Use it for | |
|---|---|---|---|
#t | |||
Show a hint
Show the solution
WITH CategoryRevenue AS ( SELECT p.CategoryId, SUM(oi.Quantity * oi.UnitPrice) AS Revenue FROM dbo.OrderItems AS oi JOIN dbo.Products AS p ON p.ProductId = oi.ProductId GROUP BY p.CategoryId),Total AS ( SELECT SUM(Revenue) AS AllRevenue FROM CategoryRevenue)SELECT c.Name, cr.Revenue, CAST(cr.Revenue * 100 / t.AllRevenue AS decimal(4,1)) AS PctOfRevenueFROM CategoryRevenue AS crJOIN dbo.Categories AS c ON c.CategoryId = cr.CategoryIdCROSS JOIN Total AS tORDER BY cr.Revenue DESC;Name Revenue PctOfRevenue -------- ------- ------------ Timber 689.60 51.7 Building 253.00 19.0 Tools 155.25 11.6 Paint 120.50 9.0 Fixings 101.30 7.6 Safety 13.60 1.0
Remember
WITH name AS (query) names a query for the statement that follows. Chain CTEs with commas; each can use the ones above it. A CTE can be referenced several times in its statement. The statement before WITH must end with a semicolon. A CTE lasts for one statement and is not stored; use a temp table for reuse. UPDATE and DELETE can target a CTE to change the underlying table.