Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›SQL›CTEs — Common Table Expressions (WITH)

Advanced Queries

CTEs — Common Table Expressions (WITH)

Common Table Expressions, or CTEs, provide a mechanism to define temporary, named result sets that exist only for the duration of a single SQL statement. They are essential for transforming complex, multi-step queries into readable, modular blocks of logic that enhance maintainability. You should reach for a CTE whenever you need to break down a long query, perform iterative calculations, or simplify complex joins that would otherwise become unmanageable.

The Anatomy of a Basic CTE

A CTE begins with the WITH keyword, followed by the name of the expression and the AS keyword, which houses the logic within a pair of parentheses. Unlike a view or a temporary table, a CTE exists only in memory during the execution of the query that immediately follows it. This makes it an ideal tool for scoping complex operations without polluting the global schema of your database. When the SQL engine encounters a CTE, it treats the resulting structure as a temporary relation that you can reference in your primary query. By isolating logical steps, you avoid the 'nested subquery' trap, where logic is buried deep within parentheses, making it difficult to debug or optimize. Because the CTE is defined before the main query, the code flows linearly, improving readability and allowing the query planner to analyze the entire execution path more effectively.

-- Define a CTE that gathers high-value customers
WITH HighValueCustomers AS (
    SELECT customer_id, SUM(amount) as total_spent
    FROM orders
    GROUP BY customer_id
    HAVING SUM(amount) > 1000
)
-- Select from the CTE as if it were a physical table
SELECT * 
FROM HighValueCustomers 
WHERE total_spent > 5000;

Chaining Multiple CTEs Together

One of the most powerful features of CTEs is the ability to chain them sequentially. By separating multiple definitions with commas, you can build a pipeline of operations where each subsequent CTE can reference the results of any previously defined CTEs. This behaves much like a set of variables in a standard procedural context, where the first operation performs a data transformation, and the second operation consumes that transformation to yield a more refined dataset. From a reasoning standpoint, this modularity allows you to test each step of your data pipeline independently. If a result is incorrect, you can simply change the final SELECT statement to target a specific intermediate CTE, effectively isolating the logic that is causing the discrepancy. This modular approach is far superior to deep subquery nesting, which obscures the logical dependency chain and makes performance tuning significantly harder for the optimizer.

-- Define two CTEs, the second uses the first
WITH RegionalSales AS (
    SELECT region, SUM(amount) as total
    FROM orders
    GROUP BY region
), 
TopRegions AS (
    -- Referencing the previous CTE
    SELECT region FROM RegionalSales WHERE total > 10000
)
SELECT * FROM TopRegions;

Using CTEs for Complex Joins

When dealing with complex analytical tasks, you often find yourself needing to perform an aggregation or a filter on a table before joining it to another, larger dataset. Without CTEs, you might end up writing large, unwieldy inline subqueries in the FROM or JOIN clauses, which are notoriously difficult to read and maintain. By isolating the pre-processing steps into a CTE, you separate the concerns of data preparation from the concerns of relational mapping. The join operation becomes much clearer because you are joining named, understandable result sets rather than opaque, multi-line subqueries. This approach also assists the SQL query planner by clearly delineating where filter conditions are applied, allowing it to potentially use indices or other optimizations on the underlying base tables before the final merge. You are essentially 'pre-calculating' the relevant slices of your data before finalizing the relationships between them.

-- Pre-processing data before a final join
WITH MonthlyStats AS (
    SELECT product_id, AVG(price) as avg_price
    FROM sales
    GROUP BY product_id
)
SELECT p.name, m.avg_price
FROM products p
JOIN MonthlyStats m ON p.id = m.product_id;

Recursive CTEs for Hierarchical Data

Recursive CTEs are a specialized form of common table expressions that reference themselves, making them the standard solution for traversing hierarchical or tree-structured data. They consist of an 'anchor' member, which provides the initial base results, and a 'recursive' member, which repeatedly queries the existing result set to find child rows. The process continues until the recursive member returns an empty set. This is mathematically and logically useful for tasks such as calculating organizational charts, bill-of-materials, or pathfinding in a graph. The reasoning here is that the state of the computation is maintained across iterations, allowing the engine to walk deeper into a structure step by step. When writing these, always ensure you have a termination condition, or the query may result in an infinite loop that consumes system resources until it hits a pre-defined recursion limit.

-- Finding an entire chain of command
WITH RECURSIVE Hierarchy AS (
    SELECT id, manager_id, name FROM employees WHERE name = 'Alice' -- Anchor
    UNION ALL
    SELECT e.id, e.manager_id, e.name -- Recursive
    FROM employees e
    JOIN Hierarchy h ON e.manager_id = h.id
)
SELECT * FROM Hierarchy;

Optimization and CTE Limitations

While CTEs are excellent for readability, it is crucial to understand that they are not inherently 'materialized' (saved to disk). In most modern SQL engines, the optimizer will 'inline' the CTE definition into the main query, essentially rewriting it back into a subquery for execution. This means that a CTE is not necessarily faster than a subquery; its primary value is structural organization. If you find yourself needing to reuse the same CTE result multiple times in a single statement, some databases support a materialized hint, though relying on this can be risky. Always check your execution plan if you suspect a complex CTE is causing performance degradation. A CTE should be viewed as a tool for human cognition and code maintainability, rather than a magic bullet for performance. When a query is complex enough to require a CTE, it is often complex enough that index strategy becomes more important than the choice between a CTE and a standard join.

-- Using a CTE to clarify a complex filter
WITH SalesVolume AS (
    SELECT customer_id, COUNT(*) as count
    FROM orders
    GROUP BY customer_id
)
SELECT c.name
FROM customers c
JOIN SalesVolume s ON c.id = s.customer_id
WHERE s.count > 10;

Key points

  • A CTE is defined using the WITH keyword and creates a temporary result set for the scope of a single query.
  • CTEs improve code readability by transforming deeply nested subqueries into a linear, modular sequence of logical steps.
  • Multiple CTEs can be defined in a single query by separating them with commas.
  • CTEs allow you to chain operations, where later CTEs can reference the results of earlier ones.
  • Recursive CTEs are specifically designed for traversing hierarchical or tree-based data structures.
  • Most database engines inline CTEs, meaning they generally perform similarly to standard subqueries or joins.
  • CTEs should be used primarily to organize code and improve maintainability rather than as a performance-tuning mechanism.
  • Recursive CTEs require an anchor member and a recursive member to build the final result set iteratively.

Common mistakes

  • Mistake: Forgetting to put the main query directly after the CTE. Why it's wrong: SQL expects a single statement to follow the CTE definition immediately; any stray commas or keywords cause syntax errors. Fix: Ensure the final SELECT/UPDATE/DELETE statement follows the CTE closing parenthesis without interruption.
  • Mistake: Trying to define a second CTE by repeating the 'WITH' keyword. Why it's wrong: 'WITH' is only used once; subsequent CTEs should be separated by commas. Fix: Use a single 'WITH' and separate multiple CTEs with commas.
  • Mistake: Assuming a CTE is persisted in the database like a Table. Why it's wrong: A CTE is a temporary result set existing only for the duration of a single statement. Fix: Recognize CTEs as temporary scopes, not schema objects.
  • Mistake: Using a CTE for recursion without a UNION ALL operator. Why it's wrong: Recursive CTEs require a base member and a recursive member joined specifically with UNION ALL to iterate. Fix: Ensure your recursive logic uses UNION ALL, not UNION or JOIN.
  • Mistake: Referencing a CTE that was defined after the current query scope. Why it's wrong: CTEs must be defined before they are referenced in the main query body. Fix: Order your CTEs from the top down, defining dependencies before the queries that use them.

Interview questions

What exactly is a Common Table Expression (CTE) and how do you define one in SQL?

A Common Table Expression, or CTE, is a temporary, named result set that you define within the execution scope of a single SELECT, INSERT, UPDATE, or DELETE statement. You define one by using the WITH keyword followed by the name of the expression and an optional column list. The main benefit is that it significantly improves code readability by breaking complex logical operations into smaller, sequential steps, making the final query much easier to maintain and troubleshoot compared to writing deeply nested subqueries.

Why would a developer prefer using a CTE over a standard subquery?

Developers often prefer CTEs because they enhance the logical flow of a query. While a subquery is tucked inside the main statement—often leading to confusing 'nested parentheses hell'—a CTE is declared at the top, acting like a temporary view. This promotes reusability within the same query scope, as you can reference the same CTE multiple times, which keeps the SQL code cleaner, more modular, and easier for other team members to debug or refactor later.

Can you compare and contrast the use of a CTE versus a Temporary Table in SQL?

The primary difference lies in persistence and scope. A CTE exists only for the duration of a single statement, effectively acting as a memory-resident alias that is optimized by the query planner. In contrast, a Temporary Table is a physical object stored in the database's temp space, allowing you to index it, insert data in multiple steps, and persist it across multiple statements within a session. Use CTEs for simple readability and Temp Tables when you need to handle large data sets, complex indexing, or multiple iterative updates.

What is a Recursive CTE, and what are the mandatory components required to construct one?

A Recursive CTE is a specialized expression that references itself to process hierarchical or tree-structured data. To construct one, you need two mandatory components joined by a UNION ALL operator. First is the 'anchor member,' which returns the base result set. Second is the 'recursive member,' which references the CTE name itself to iterate through the data. It also requires a termination condition, usually a WHERE clause, to stop the recursion and prevent an infinite loop, which is essential for traversing organizational charts or folder structures.

How does the query optimizer treat a CTE versus a subquery in terms of execution plan?

In many modern SQL engines, the query optimizer treats a non-recursive CTE and an equivalent subquery identically; it essentially 'flattens' the logic into a single execution plan. However, the advantage of the CTE is that it organizes the operation into a distinct block that the optimizer can analyze as a logical unit. While neither inherently guarantees better performance than the other, the CTE allows for cleaner query architecture, which often helps the optimizer make more informed decisions about how to join tables and filter records efficiently.

In a scenario involving complex data transformation, how do you chain multiple CTEs together, and what are the performance implications?

You can chain multiple CTEs by separating them with commas immediately following the initial WITH keyword. For example: 'WITH CTE1 AS (...), CTE2 AS (SELECT * FROM CTE1 ...)'. This creates a pipeline of transformations. From a performance standpoint, while this is excellent for legibility, SQL engines may re-evaluate a CTE each time it is referenced. If a CTE is expensive to compute and used multiple times, the engine might re-run that logic repeatedly, so keep an eye on execution plans for signs of redundant computation.

All SQL interview questions →

Check yourself

1. What is the primary benefit of using a CTE instead of a subquery in a SELECT statement?

  • A.CTEs automatically cache the data on the disk for faster retrieval
  • B.CTEs improve code readability and allow for self-referencing via recursion
  • C.CTEs are required for all aggregate functions to work correctly
  • D.CTEs allow you to bypass primary key constraints during a join
Show answer

B. CTEs improve code readability and allow for self-referencing via recursion
CTEs provide a clear, linear structure for complex logic, and they are the only mechanism for recursive queries. Options 1, 3, and 4 are false as CTEs do not cache on disk, are not required for aggregates, and do not bypass constraints.

2. How do you define multiple CTEs in a single SQL statement?

  • A.By repeating the WITH keyword for each expression
  • B.By defining them in separate batches using the GO command
  • C.By separating the expressions with a comma after the first WITH
  • D.By nesting each CTE inside the previous one's body
Show answer

C. By separating the expressions with a comma after the first WITH
Standard SQL syntax for multiple CTEs uses a single 'WITH' followed by comma-separated definitions. Option 1 is invalid syntax, option 2 breaks the statement scope, and option 4 is not how SQL handles multiple result sets.

3. Which of the following is required for a recursive CTE to function correctly?

  • A.A constant hardcoded table reference
  • B.An anchor member, a recursive member, and UNION ALL
  • C.A stored procedure to handle the loop index
  • D.The use of the RECURSIVE keyword in all SQL dialects
Show answer

B. An anchor member, a recursive member, and UNION ALL
Recursive CTEs must have a base case (anchor), a recursive part that references the CTE itself, and the UNION ALL operator. Other options are irrelevant or syntactically incorrect for standard SQL implementations.

4. What happens to the data defined in a CTE once the main query completes?

  • A.It is saved as a temporary table in the tempdb
  • B.It remains in memory until the database session is closed
  • C.It is discarded immediately as it exists only for the duration of the query
  • D.It is moved to a permanent audit table
Show answer

C. It is discarded immediately as it exists only for the duration of the query
CTEs are volatile and scope-limited; they vanish as soon as the statement finishes. They are not saved to disk or persistent memory as described in the other choices.

5. If you have a CTE named 'Sales_CTE', where can you reference it?

  • A.Only in the immediate query that follows the CTE definition
  • B.In any subsequent query executed by any user in the database
  • C.Only within the stored procedure where it was defined
  • D.In the FROM clause of the main query or subsequent CTEs
Show answer

D. In the FROM clause of the main query or subsequent CTEs
A CTE is scoped to the query it belongs to; it can be referenced in the main query or in later CTE definitions within the same WITH block. It is not global (option 2) and is not restricted to stored procedures (option 3).

Take the full SQL quiz →

← PreviousSubqueries — Scalar, Row, TableNext →Window Functions — ROW_NUMBER, RANK, DENSE_RANK

SQL

32 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app