<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. SQL
  3. Grouping and subqueries
All lessons in this course

Lesson 28 of 54 15 minIntermediate

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.

A common table expression (CTE) gives a query a name, at the top of a statement, so the main query can use it like a table. It is a derived table moved to the front, where it is easier to read:

Query.sql
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;
Output
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.50

Read it top to bottom: first work out each order’s total, then use those totals. With a derived table, the first step would be buried in the middle of the FROM clause.

Several CTEs, one step at a time

Separate several CTEs with commas. Each one can use the ones above it, so a complicated question becomes a series of small, named steps:

Query.sql
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;
Output
FirstName  Orders   Spent    AvgSpent
---------  ------  ------  ----------
Dev             2  723.50  263.930000
Interactive

A query as named steps

CustomerTotals.sql
SELECT o.CustomerId, COUNT(*) AS Orders, SUM(t.Total) AS SpentFROM OrderTotals AS tJOIN dbo.Orders AS o ON o.OrderId = t.OrderIdWHERE o.Status <> 'Cancelled'GROUP BY o.CustomerId
CustomerIdOrdersSpent
112150.80
222131.00
342723.50
462211.85
581102.50
Step 2 reads step 1 as if it were a table, drops Chloe’s cancelled order, and adds up each customer’s orders. Customers without orders never appear here.

Naming the columns

You can name a CTE’s columns in brackets after its name instead of with aliases inside. Every column needs a name one way or the other:

Query.sql
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;
Output
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

A CTE can be referenced more than once in the main query, which a derived table cannot without being written out again. Comparing each month with the month before means joining the monthly figures to themselves:

Query.sql
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;
Output
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.65

The semicolon rule

WITH has other meanings in T-SQL (such as table hints), so the statement before a CTE must end with a semicolon:

Query.sql
SELECT COUNT(*) AS Orders FROM dbo.OrdersWITH Recent AS (SELECT * FROM dbo.Orders WHERE OrderDate >= '2024-06-01')SELECT OrderId FROM Recent;
Output
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.

That is why you often see CTEs written as ;WITH. Ending every statement with a semicolon, as this course does, avoids the problem.

A CTE lives for one statement

A CTE exists only for the single statement that follows it. The next statement cannot see it:

Query.sql
WITH Pending AS (SELECT OrderId FROM dbo.Orders WHERE Status = 'Pending')SELECT COUNT(*) AS PendingOrders FROM Pending; SELECT * FROM Pending;
Output
PendingOrders
-------------
            2

Msg 208, Level 16, State 1, Line 4
Invalid object name 'Pending'.

Changing data through a CTE

The statement after a CTE can be an UPDATE or DELETE, which changes the underlying table. This is a clear way to say “find these rows, then change them”:

Query.sql
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);
Output
Name              Stock
----------------  -----
Wall plugs (100)     25
Safety goggles       25

CTE, derived table, temp table or view?

Lives forReusableUse it for
Derived tableone querynoa small one-off step inside FROM
CTEone statementwithin that statementreadable step-by-step queries, recursion
Temp table #tthe sessionby later statementsa result that is expensive or needed many times; it can be indexed
Viewuntil droppedby anyonea query the whole team uses (the Views module)
Quick check

A batch has two statements after a CTE definition. Which of them can use the CTE?

Quick check

SELECT 1 AS x is followed on the next line by WITH t AS (…) SELECT …, and the batch fails. Why?

Practice

Using two CTEs, list each category name with its share of total revenue as a percentage (one decimal place). The first CTE should total revenue per category; the second the grand total. Include only categories that have sales, highest share first.

Show a hint

CategoryRevenue: join OrderItems to Products, GROUP BY CategoryId. Total: SELECT SUM(Revenue) FROM CategoryRevenue. Then join Categories and CROSS JOIN Total.

Show the solution
Solution.sql
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;
Output
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.
0 of 54 lessons completed