Common Table Expressions
CTE
Recursive CTEs
Activity Solutions
A common table expression (CTE) names a query result that you can reference within a single SQL statement.
WITH.A CTE helps organize query logic. It does not create a permanent table.
large_invoices.
Invoice table.
large_invoices.
| Feature | Subquery | CTE |
|---|---|---|
| Readability | Low (Nested) | High (Sequential) |
| Reusability | Can only be used once | Can be referenced multiple times |
| Debugging | Difficult | Easy (Test CTE separately) |
| Complexity | Hard to manage | Great for complex logic |
Question: Which billing countries have total sales above 200?
country_sales.
Invoice table.
country_sales.
WITH country_stats AS (
SELECT BillingCountry, COUNT(*) AS invoice_count,
SUM(Total) AS total_sales
FROM Invoice
GROUP BY BillingCountry
),
busy_countries AS (
SELECT * FROM country_stats
WHERE invoice_count >= 20
)
SELECT BillingCountry, invoice_count,
ROUND(total_sales, 2) AS total_sales
FROM busy_countries
ORDER BY total_sales DESC
LIMIT 3;country_stats, to summarize invoices by billing country.
Invoice table.
busy_countries. A comma separates the two CTE definitions.
Use one WITH and commas between CTE definitions.
Question: Which customers have spent the most?
WITH customer_spending AS (
SELECT CustomerId, SUM(Total) AS total_spent
FROM Invoice
GROUP BY CustomerId
)
SELECT c.FirstName || ' ' || c.LastName AS customer,
ROUND(s.total_spent, 2) AS total_spent
FROM Customer AS c
JOIN customer_spending AS s ON c.CustomerId = s.CustomerId
ORDER BY s.total_spent DESC, c.CustomerId
LIMIT 5;customer_spending to summarize spending by customer.
Invoice table.
customer.
Customer, using the alias c.
s refers to customer_spending.
The CTE summarizes invoices. The join adds customer names.
A. Sales by billing country
Create a CTE with each billing country’s invoice count and total sales. Return countries with at least 20 invoices. Sort by total sales from largest to smallest.
B. Larger average invoices
Use two CTEs: first calculate invoice counts and average invoice totals by billing country, then keep countries with at least 20 invoices. Return those with an average invoice total above 5.50, sorted by average total from largest to smallest.
CTE
Recursive CTEs
Activity Solutions
Recursion is a process that uses its own previous result to produce the next result. It needs a starting point and a condition that stops it.
| Part | Purpose |
|---|---|
WITH RECURSIVE |
Introduces the CTE definitions |
| Anchor query | Produces the starting rows |
UNION ALL |
Combines generated rows, keeping duplicates (Adds Rows to the new table) |
| Recursive member | References the CTE to generate more rows |
| Termination rule | Prevents further expansion when finished |
| Final query | Selects and orders the result |
The anchor and recursive member must return the same number of columns in corresponding positions.
Result: 1, 2, 3, 4, 5.
numbers(n) explicitly names the CTE’s output column.
| Current row | Does n < 5 hold? |
New row |
|---|---|---|
| Anchor | Not evaluated | 1 |
| 1 | Yes | 2 |
| 2 | Yes | 3 |
| 3 | Yes | 4 |
| 4 | Yes | 5 |
| 5 | No | None |
The condition checks the current n before producing n + 1.
Predict: What would WHERE n <= 5 produce?
| Level below Andrew | Employees | Direct manager |
|---|---|---|
| 0 | Andrew Adams | None |
| 1 | Nancy Edwards, Michael Mitchell | Andrew Adams |
| 2 | Jane Peacock, Margaret Park, Steve Johnson | Nancy Edwards |
| 2 | Robert King, Laura Callahan | Michael Mitchell |
A direct report is one link below a manager.
A descendant can be any number of links below that manager.
depth will count links from the chosen starting employee.
Employee Table
WITH RECURSIVE team(EmployeeId, employee_name, depth) AS (
SELECT EmployeeId, FirstName || ' ' || LastName, 0
FROM Employee
WHERE EmployeeId = 1
UNION ALL
SELECT e.EmployeeId, e.FirstName || ' ' || e.LastName,
t.depth + 1
FROM Employee AS e
JOIN team AS t ON e.ReportsTo = t.EmployeeId
)
SELECT employee_name, depth
FROM team
WHERE depth > 0
ORDER BY depth, employee_name;team with three columns: employee ID, employee name, and depth.
||, and assign depth 0.
Employee table.
1—Andrew Adams in Chinook—as the starting point.
Employee, using e as a short alias.
team. Each match finds a direct report; repeating this step finds employees farther down the hierarchy.
team result.
0, leaving only their descendants.
| employee_name | depth |
|---|---|
| Michael Mitchell | 1 |
| Nancy Edwards | 1 |
| Jane Peacock | 2 |
| Laura Callahan | 2 |
| Margaret Park | 2 |
| Robert King | 2 |
| Steve Johnson | 2 |
WITH RECURSIVE chain AS (
SELECT EmployeeId, FirstName, LastName, ReportsTo,
0 AS depth
FROM Employee WHERE EmployeeId = 8
UNION ALL
SELECT e.EmployeeId, e.FirstName, e.LastName, e.ReportsTo,
c.depth + 1
FROM Employee AS e
JOIN chain AS c ON e.EmployeeId = c.ReportsTo
)
SELECT FirstName || ' ' || LastName AS manager, depth
FROM chain
WHERE depth > 0
ORDER BY depth;chain to trace an employee’s managers.
ReportsTo).
0.
8 as the starting point.
Employee, using the alias e.
chain: match the manager’s EmployeeId to that person’s ReportsTo. This moves up the hierarchy.
manager, and include depth.
chain.
depth = 1) upward through the hierarchy.
A recursive query ends when no unprocessed rows remain.
For these examples, expansion stops when:
n < 5.| Operator | Treatment of generated rows |
|---|---|
UNION ALL |
Keeps repeated rows |
UNION |
Discards duplicate complete rows |
A. Nancy’s reports
Start with Nancy Edwards (EmployeeId = 2). Return her descendants and their depths, excluding Nancy. Sort by depth and name.
B. Jane’s managers
Start with Jane Peacock (EmployeeId = 3) and move upward through her managers.
C. Andrew’s report counts
Start with Andrew Adams (EmployeeId = 1). Count his descendants at each depth. Exclude depth 0 and sort by depth.
CTE
Recursive CTEs
Activity Solutions
country_sales to summarize invoices by billing country.
Invoice table.
WITH country_summary AS (
SELECT BillingCountry,
COUNT(*) AS invoice_count,
AVG(Total) AS average_total
FROM Invoice
GROUP BY BillingCountry
),
eligible_countries AS (
SELECT BillingCountry, invoice_count, average_total
FROM country_summary
WHERE invoice_count >= 20
)
SELECT BillingCountry, invoice_count, average_total
FROM eligible_countries
WHERE average_total > 5.50
ORDER BY average_total DESC;country_summary, to summarize invoices by billing country.
eligible_countries. A comma separates the two CTE definitions.
WITH RECURSIVE team AS (
SELECT EmployeeId, FirstName || ' ' || LastName AS employee_name,
0 AS depth
FROM Employee
WHERE EmployeeId = 2
UNION ALL
SELECT e.EmployeeId, e.FirstName || ' ' || e.LastName,
t.depth + 1
FROM Employee AS e
JOIN team AS t ON e.ReportsTo = t.EmployeeId
)
SELECT employee_name, depth
FROM team
WHERE depth > 0
ORDER BY depth, employee_name;team.
0.
ReportsTo matches the current manager’s ID.
WITH RECURSIVE chain AS (
SELECT EmployeeId, FirstName, LastName, ReportsTo,
0 AS depth
FROM Employee
WHERE EmployeeId = 3
UNION ALL
SELECT e.EmployeeId, e.FirstName, e.LastName, e.ReportsTo,
c.depth + 1
FROM Employee AS e
JOIN chain AS c ON e.EmployeeId = c.ReportsTo
)
SELECT FirstName || ' ' || LastName AS manager, depth
FROM chain
WHERE depth > 0
ORDER BY depth;ReportsTo) so the recursive query can find the next manager. Start at depth 0.
WITH RECURSIVE descendants AS (
SELECT EmployeeId, 0 AS depth
FROM Employee
WHERE EmployeeId = 1
UNION ALL
SELECT e.EmployeeId, d.depth + 1
FROM Employee AS e
JOIN descendants AS d ON e.ReportsTo = d.EmployeeId
)
SELECT depth, COUNT(*) AS descendant_count
FROM descendants
WHERE depth > 0
GROUP BY depth
ORDER BY depth;descendants.
0.
0.