SQL CTEs: Write Clearer Queries with Common Table Expressions

Transform complex nested queries into readable, maintainable SQL. Learn 6 powerful CTE patterns including recursive CTEs for hierarchical data.

350+
SQL Examples
140+
Interview Questions
270+
Pages
4.8/5
Reader Rating

Why CTEs Make Your SQL Better

Complex queries with multiple subqueries quickly become unreadable. You end up with deeply nested SELECT statements where you lose track of what each level does. Debugging these queries is painful, and modifying them later is risky. CTEs solve this by letting you break complex logic into named, sequential steps.

Common Table Expressions (CTEs) use the WITH keyword to define temporary named result sets at the beginning of your query. Instead of nesting subqueries inside subqueries, you write each step at the top with a descriptive name. The main query then references these names, making the logic flow clear and easy to follow.

Beyond readability, CTEs enable recursive queries for hierarchical data. Organization charts, bill of materials, category trees, and other parent-child relationships of unknown depth are natural fits for recursive CTEs. Without them, you'd need procedural code or awkward self-joins limited to fixed depths.

This guide covers six essential CTE patterns that handle common scenarios: simplifying complex queries, chaining transformations, navigating hierarchies, and optimizing repeated calculations. Understanding CTEs elevates your SQL from functional to maintainable.

Common Mistakes with CTEs

Forgetting the Comma Between Multiple CTEs

When defining multiple CTEs, separate them with commas, not additional WITH keywords. It's WITH cte1 AS (...), cte2 AS (...), not WITH cte1 AS (...) WITH cte2 AS (...). This syntax error trips up beginners who expect each CTE to start with WITH.

Creating Infinite Loops in Recursive CTEs

Recursive CTEs without proper termination conditions run forever (or until hitting recursion limits). Always ensure your recursive query eventually produces no rows. Test with small datasets first and include safety conditions to prevent runaway recursion.

Using CTEs When Views Would Be Better

If you're writing the same CTE in multiple queries, create a view instead. CTEs are query-scoped and don't persist. Views are reusable database objects. Use CTEs for query-specific logic, views for shared logic across multiple queries.

Expecting CTEs to Always Improve Performance

CTEs don't guarantee better performance. Most databases treat them like subqueries. While they can help when you reference the same result set multiple times, the optimizer may inline them anyway. Use CTEs for readability first, performance second. Always verify with EXPLAIN plans.

Referencing CTEs in the Wrong Order

You can only reference CTEs defined earlier in the WITH clause. If cte2 references cte1, define cte1 first. Order matters when chaining CTEs. The database evaluates them sequentially, so forward references cause errors.

6 Essential CTE Patterns

These patterns demonstrate how CTEs simplify complex queries and enable powerful recursive operations.

Basic CTE for Query Clarity

Simplify complex queries by breaking them into readable steps

Example:

WITH recent_orders AS (
  SELECT customer_id, order_id, total_amount, order_date
  FROM orders
  WHERE order_date >= '2025-01-01'
)
SELECT
  c.customer_name,
  COUNT(ro.order_id) as order_count,
  SUM(ro.total_amount) as total_spent
FROM customers c
JOIN recent_orders ro ON c.customer_id = ro.customer_id
GROUP BY c.customer_id, c.customer_name
ORDER BY total_spent DESC;

When to Use:

CTEs (Common Table Expressions) make queries more readable by giving names to subqueries. Instead of nested subqueries, you define temporary result sets at the top. The query becomes self-documenting - the name 'recent_orders' tells you exactly what the data represents.

Multiple CTEs for Complex Logic

Chain multiple CTEs together for step-by-step transformations

Example:

WITH active_customers AS (
  SELECT customer_id, customer_name
  FROM customers
  WHERE status = 'active'
),
customer_orders AS (
  SELECT
    ac.customer_id,
    ac.customer_name,
    COUNT(o.order_id) as order_count,
    SUM(o.total_amount) as total_spent
  FROM active_customers ac
  LEFT JOIN orders o ON ac.customer_id = o.customer_id
  GROUP BY ac.customer_id, ac.customer_name
)
SELECT *
FROM customer_orders
WHERE order_count >= 5 OR total_spent > 1000
ORDER BY total_spent DESC;

When to Use:

Chain multiple CTEs by separating them with commas. Each CTE can reference previous ones, building complex logic step by step. This approach is much clearer than nested subqueries and easier to debug - you can comment out the final SELECT and test intermediate CTEs.

Recursive CTE for Hierarchical Data

Navigate tree structures like org charts or category hierarchies

Example:

WITH RECURSIVE employee_hierarchy AS (
  -- Anchor: start with top-level employees
  SELECT employee_id, employee_name, manager_id, 1 as level
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- Recursive: get employees who report to current level
  SELECT e.employee_id, e.employee_name, e.manager_id, eh.level + 1
  FROM employees e
  JOIN employee_hierarchy eh ON e.manager_id = eh.employee_id
)
SELECT * FROM employee_hierarchy
ORDER BY level, employee_name;

When to Use:

Recursive CTEs solve hierarchical problems like org charts, bill of materials, or category trees. The anchor query defines the starting point, then the recursive part keeps adding rows until no more are found. Essential for navigating parent-child relationships of unknown depth.

CTE for Data Transformation Pipeline

Transform data through multiple stages in one query

Example:

WITH sales_data AS (
  SELECT
    product_id,
    SUM(quantity) as total_quantity,
    SUM(quantity * price) as total_revenue
  FROM order_items
  GROUP BY product_id
),
enriched_data AS (
  SELECT
    p.product_name,
    p.category,
    sd.total_quantity,
    sd.total_revenue,
    sd.total_revenue / NULLIF(sd.total_quantity, 0) as avg_price
  FROM sales_data sd
  JOIN products p ON sd.product_id = p.product_id
),
categorized_data AS (
  SELECT
    *,
    CASE
      WHEN total_revenue > 10000 THEN 'High Performer'
      WHEN total_revenue > 5000 THEN 'Medium Performer'
      ELSE 'Low Performer'
    END as performance_tier
  FROM enriched_data
)
SELECT * FROM categorized_data
ORDER BY total_revenue DESC;

When to Use:

Use multiple CTEs to transform data through stages: aggregate raw data, enrich with lookups, add calculated fields, categorize results. This pipeline approach is cleaner than a single complex query and easier to maintain. Each stage has a clear purpose.

CTE for Self-Joins Simplification

Make self-joins more readable with named result sets

Example:

WITH customer_stats AS (
  SELECT
    customer_id,
    COUNT(*) as order_count,
    AVG(total_amount) as avg_order_value
  FROM orders
  GROUP BY customer_id
)
SELECT
  cs1.customer_id as customer_1,
  cs2.customer_id as customer_2,
  ABS(cs1.avg_order_value - cs2.avg_order_value) as value_difference
FROM customer_stats cs1
JOIN customer_stats cs2 ON cs1.customer_id < cs2.customer_id
WHERE ABS(cs1.avg_order_value - cs2.avg_order_value) < 10
ORDER BY value_difference;

When to Use:

CTEs make self-joins clearer by defining the dataset once, then joining it to itself with meaningful aliases. Instead of repeating the same subquery twice, you write it once in the CTE. Particularly useful when comparing rows within the same table.

CTE for Performance Optimization

Materialize intermediate results to avoid repeated calculations

Example:

WITH expensive_calculation AS (
  SELECT
    date_trunc('month', order_date) as month,
    customer_id,
    COUNT(*) as order_count,
    SUM(total_amount) as monthly_total,
    AVG(total_amount) as avg_order
  FROM orders
  WHERE order_date >= '2024-01-01'
  GROUP BY date_trunc('month', order_date), customer_id
)
SELECT
  month,
  SUM(order_count) as total_orders,
  AVG(monthly_total) as avg_customer_spend,
  COUNT(DISTINCT customer_id) as active_customers
FROM expensive_calculation
GROUP BY month
ORDER BY month;

When to Use:

CTEs can improve performance by computing expensive operations once instead of multiple times. While not materialized by default in all databases, they provide a clear point for the query optimizer. Some databases (like PostgreSQL 12+) can inline or materialize CTEs based on performance.

Your First Steps with CTEs

Step 1: Convert a Subquery to a CTE

Take a query with a subquery in FROM or WHERE clause and rewrite it as a CTE. This helps you understand the basic syntax and see how CTEs improve readability.

-- Before: subquery in FROM
SELECT customer_name, order_count
FROM (
  SELECT customer_id, COUNT(*) as order_count
  FROM orders GROUP BY customer_id
) o
JOIN customers c ON o.customer_id = c.customer_id;

-- After: CTE
WITH order_counts AS (
  SELECT customer_id, COUNT(*) as order_count
  FROM orders GROUP BY customer_id
)
SELECT c.customer_name, oc.order_count
FROM order_counts oc
JOIN customers c ON oc.customer_id = c.customer_id;

Step 2: Chain Multiple CTEs Together

Practice writing queries with 2-3 CTEs that build on each other. This teaches you how to structure complex transformations as a series of clear steps.

WITH step1 AS (
  SELECT * FROM orders WHERE order_date >= '2025-01-01'
),
step2 AS (
  SELECT customer_id, COUNT(*) as count FROM step1 GROUP BY customer_id
),
step3 AS (
  SELECT * FROM step2 WHERE count >= 5
)
SELECT c.customer_name, s.count
FROM step3 s
JOIN customers c ON s.customer_id = c.customer_id;

Step 3: Try a Simple Recursive CTE

Start with a basic recursive CTE like generating a series of numbers. Once you understand the anchor and recursive parts, move to real hierarchical data.

-- Generate numbers 1 to 10
WITH RECURSIVE numbers AS (
  SELECT 1 as n
  UNION ALL
  SELECT n + 1 FROM numbers WHERE n < 10
)
SELECT * FROM numbers;

Frequently Asked Questions About CTEs

What is the difference between a CTE and a subquery?

CTEs and subqueries are functionally similar - both create temporary result sets. CTEs are written at the top with WITH and can be referenced multiple times in the query. Subqueries are inline and must be repeated if needed multiple times. CTEs are generally more readable, especially for complex queries. Use CTEs when you need to reference the same result set multiple times or when readability matters.

Are CTEs faster than subqueries?

Not necessarily. Most databases treat simple CTEs similarly to subqueries in terms of performance. However, if you reference a CTE multiple times, some databases compute it once rather than executing the same subquery multiple times. The main benefit of CTEs is readability and maintainability, not performance. Always test with EXPLAIN to verify performance.

Can I use CTEs for UPDATE or DELETE statements?

Yes, CTEs work with INSERT, UPDATE, and DELETE, not just SELECT. For example: WITH rows_to_delete AS (SELECT id FROM table WHERE condition) DELETE FROM table WHERE id IN (SELECT id FROM rows_to_delete). This makes complex modifications more readable and testable.

How do recursive CTEs work?

Recursive CTEs have two parts: an anchor query (the starting point) and a recursive query (that references the CTE itself). The database executes the anchor first, then repeatedly executes the recursive part using results from the previous iteration until no new rows are produced. Used for hierarchies, graphs, and iterative calculations.

Can I reference one CTE from another CTE?

Yes, when you define multiple CTEs separated by commas, later CTEs can reference earlier ones. This creates a pipeline where each CTE builds on previous results. The order matters - you can only reference CTEs defined earlier in the WITH clause.

What is the maximum recursion depth for recursive CTEs?

Most databases have a recursion limit to prevent infinite loops. PostgreSQL defaults to unlimited (but uses memory limits). SQL Server defaults to 100. MySQL defaults to 1000. You can usually adjust this with database-specific settings. Always include a termination condition in your recursive query to prevent runaway recursion.

When should I use CTEs instead of views?

Use CTEs for query-specific temporary results that you don't need to reuse across multiple queries. Use views for reusable logic that multiple queries or users need to access. CTEs exist only for the duration of one query. Views are permanent database objects that multiple queries can use.

Can CTEs improve query performance?

CTEs primarily improve readability, not performance. In some cases they can help performance by avoiding repeated calculations when referenced multiple times. However, some databases inline CTEs (treat them like subqueries) which negates this benefit. PostgreSQL 12+ introduced MATERIALIZED CTEs for explicit control over this behavior.

Write Better SQL with CTEs

This guide covers essential CTE patterns. The complete book provides comprehensive SQL training from fundamentals to expert techniques, with hundreds of practical examples.

What You'll Get:

  • 350+ practical SQL examples covering all skill levels
  • 140+ SQL interview questions with detailed solutions
  • Complete e-commerce database for hands-on practice
  • SQL code library with 380+ ready-to-use statements
  • 2 bonus SQL cheat sheets: Glossary and Dialect Differences

Trusted by developers worldwide

4.8/5 rating - Bestselling SQL book