Skip to main content

CTEs and the WITH Clause

Learning Focus

Use this lesson to master CTEs — named subqueries that make complex SQL readable and reusable.

What is a CTE?

A Common Table Expression (CTE) is a named, temporary result set defined at the start of a query using the WITH keyword. It can be referenced like a table within the main query.

Benefits:

  • Break complex queries into readable steps
  • Reference the same subquery multiple times without repeating it
  • Required for recursive queries

Basic Syntax

WITH cte_name AS (
SELECT ... FROM ...
)
SELECT * FROM cte_name;

Examples

Example 1: Basic CTE

WITH high_earners AS (
SELECT name, department, salary
FROM employees
WHERE salary > 80000
)
SELECT department, COUNT(*) AS count, AVG(salary) AS avg_salary
FROM high_earners
GROUP BY department;

Example 2: Multiple CTEs (Chained)

WITH
dept_stats AS (
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
),
above_avg_depts AS (
SELECT department
FROM dept_stats
WHERE avg_salary > 80000
)
SELECT e.name, e.salary, e.department
FROM employees e
INNER JOIN above_avg_depts d ON e.department = d.department
ORDER BY e.salary DESC;

Example 3: CTE for Deduplication

WITH ranked AS (
SELECT *,
row_number() OVER (PARTITION BY email ORDER BY created_at DESC) AS rn
FROM users
)
SELECT * FROM ranked WHERE rn = 1;

Example 4: Recursive CTE (Hierarchy)

-- Employee → Manager hierarchy traversal
WITH RECURSIVE emp_hierarchy AS (
-- Base case: top-level managers (no manager)
SELECT id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL

UNION ALL

-- Recursive: employees reporting to someone in hierarchy
SELECT e.id, e.name, e.manager_id, h.level + 1
FROM employees e
INNER JOIN emp_hierarchy h ON e.manager_id = h.id
)
SELECT name, level FROM emp_hierarchy ORDER BY level, name;

CTE vs Subquery

AspectCTESubquery
ReadabilityHigh — named and reusableLow — nested and inlined
ReuseCan reference multiple timesMust repeat
RecursiveSupportedNot supported
PerformanceDuckDB materializes CTEs when referenced multiple timesInline execution

Common Pitfalls

PitfallConsequencePrevention
CTE column not foundError — CTE isn't in scopeCTEs are only scoped to the query they're in
Duplicate CTE namesErrorEach CTE must have a unique name
Recursion without base caseInfinite loopAlways include a WHERE termination condition

Quick Reference

WITH cte AS (SELECT ...)
SELECT * FROM cte;

WITH a AS (...), b AS (SELECT * FROM a WHERE ...)
SELECT * FROM b;

WITH RECURSIVE r AS (SELECT ... UNION ALL SELECT ... FROM r WHERE ...)
SELECT * FROM r;

What's Next