Data AnalyticsSQL for Analyticsbeginner
Updated:

SQL CTEs for Data Analysts

4 min read

Complex reports read better as named steps. Learn the CTE (WITH clause) to structure multi-stage analytics queries clearly and reuse intermediate results.

TL;DR – Quick Answer

A CTE, or common table expression, is a named temporary result defined with the WITH keyword at the top of a query, then referenced like a table in the main query. Analysts use CTEs to break a complex report into readable, named steps that run top to bottom, to reuse an intermediate result more than once, and to write recursive queries for hierarchies. They make the same logic as nested subqueries far easier to read and debug.

On This Page

As reports grow, single queries turn into tangled nests of subqueries that nobody — including their author a week later — can read. Common table expressions, written with the WITH keyword, fix this. A CTE is a named intermediate result you define at the top of the query and then use like a table below. The logic reads top to bottom, each step has a name, and you can reference a step more than once. For an analyst who writes long reporting queries daily, CTEs are the difference between maintainable and unmaintainable SQL.

This tutorial builds on subqueries — a CTE is essentially a named subquery. It sits in the intermediate stretch of the SQL for analytics path.

The basic WITH syntax

Define a CTE with WITH name AS (query), then select from it:

-- orders
order_id | customer | city      | amount
---------+----------+-----------+-------
1001     | Aarti    | Hyderabad | 1200
1002     | Bhaskar  | Hyderabad | 8500
1003     | Chitra   | Pune      | 1200
1004     | Devan    | Chennai   | 600
1005     | Esha     | Pune      | 4000

"Total revenue per city, then only cities above 5000" reads cleanly as a CTE:

WITH city_revenue AS (
    SELECT city, SUM(amount) AS revenue
    FROM orders
    GROUP BY city
)
SELECT city, revenue
FROM city_revenue
WHERE revenue > 5000
ORDER BY revenue DESC;
city      | revenue
----------+--------
Hyderabad | 9700

The city_revenue CTE computes the per-city totals; the main query filters them. The exact same result is possible with a subquery in FROM, but the CTE names the intermediate step, so the intent is obvious at a glance.

Chaining CTEs into a pipeline

The real power appears when you chain CTEs, each building on the last. Separate them with commas under one WITH. Here is a two-step pipeline: aggregate per city, then rank and label them.

WITH city_revenue AS (
    SELECT city, SUM(amount) AS revenue
    FROM orders
    GROUP BY city
),
ranked AS (
    SELECT city, revenue,
           RANK() OVER (ORDER BY revenue DESC) AS revenue_rank
    FROM city_revenue
)
SELECT city, revenue, revenue_rank
FROM ranked
WHERE revenue_rank <= 2;
city      | revenue | revenue_rank
----------+---------+-------------
Hyderabad | 9700    | 1
Pune      | 5200    | 2

Each CTE reads like a paragraph: "first compute city revenue, then rank it, then keep the top two." Compare that to the same logic crammed into nested subqueries — the CTE version is far easier to write correctly and to hand to a colleague. The RANK() OVER (...) here is a window function, which pairs with CTEs constantly.

Referencing a CTE more than once

An inline subquery must be repeated everywhere it is used; a CTE can be named once and referenced multiple times. To compare each city's revenue to the overall average:

WITH city_revenue AS (
    SELECT city, SUM(amount) AS revenue
    FROM orders
    GROUP BY city
)
SELECT city, revenue,
       (SELECT AVG(revenue) FROM city_revenue) AS avg_revenue
FROM city_revenue
ORDER BY revenue DESC;

city_revenue is used both in the main FROM and inside the scalar subquery, without duplicating its definition. This reuse is a genuine advantage over inline subqueries when the same intermediate result feeds several parts of a query.

Recursive CTEs for hierarchies

A recursive CTE references itself to walk hierarchical data of unknown depth — an org chart, a category tree — which a single self join cannot handle. It has a base case and a recursive step unioned together:

WITH RECURSIVE chain AS (
    SELECT emp_id, emp_name, manager_id, 1 AS level
    FROM employees
    WHERE manager_id IS NULL          -- base case: the top
    UNION ALL
    SELECT e.emp_id, e.emp_name, e.manager_id, c.level + 1
    FROM employees e
    JOIN chain c ON e.manager_id = c.emp_id   -- recursive step
)
SELECT emp_name, level FROM chain ORDER BY level;

The base case selects the top-level employee; the recursive step repeatedly attaches each person's reports, incrementing a level counter until no new rows are found. Recursive CTEs solve multi-level problems cleanly, though most day-to-day analyst reporting uses ordinary, non-recursive CTEs.

Common mistakes

  • Expecting a CTE to persist. A CTE exists only for the single statement that defines it. It is not a temporary table you can reuse in the next query.
  • Assuming CTEs are always faster. They usually match subquery performance, but some databases materialize them, which can hurt. Do not adopt CTEs for speed; adopt them for clarity.
  • Recursive CTE with no termination. If the recursive step never stops adding rows — for instance, a cycle in the hierarchy — the query runs away. Ensure the data is a true tree or add a depth guard.
  • Over-splitting trivial queries. Wrapping a one-line query in three CTEs adds noise. Use CTEs when they genuinely clarify multi-step logic.

In interviews

CTE questions test structured thinking: "using CTEs, find the top two cities by revenue and their rank", or "walk the management chain from a given employee". Interviewers like CTEs because a candidate's version reveals how they decompose a problem into named steps. A strong answer names each CTE meaningfully and explains why the pipeline reads more clearly than nested subqueries. Recursive CTEs come up specifically for hierarchy questions.

Where this fits in your learning path

CTEs are the readable form of the subqueries you learned earlier, and they pair naturally with window functions for ranked, multi-step reports. For single-level hierarchies a self join still suffices; recursive CTEs extend that to any depth. Structuring queries well with CTEs is an expected skill on the data analyst roadmap.

Frequently Asked Questions

What is a CTE in SQL?
A CTE is a named temporary result set defined with WITH at the start of a query and referenced by name in the main query. It exists only for that single statement. Analysts use CTEs to structure multi-step reports into readable named blocks instead of deeply nested subqueries.
What is the difference between a CTE and a subquery?
They are functionally similar, but a CTE is named and defined at the top with WITH, so the query reads top to bottom, while a subquery is inline. A CTE can also be referenced multiple times in the same query, whereas an inline subquery would have to be repeated. CTEs win on readability for complex logic.
Can I chain multiple CTEs together?
Yes. Separate them with commas after a single WITH, and each later CTE can reference earlier ones. This lets you build a pipeline: one CTE aggregates, the next ranks the aggregates, the final query filters the ranking. Chained CTEs are how analysts structure long reports clearly.
Are CTEs slower than subqueries?
Usually performance is similar because many databases optimize CTEs the same way as subqueries. Some databases materialize a CTE, which can help or hurt depending on the case. For most analyst reports the readability benefit outweighs any small performance difference; profile only if a query is genuinely slow.
What is a recursive CTE used for?
A recursive CTE references itself to walk hierarchical data, such as an org chart from a top manager down through all levels of reports, or a category tree. It has a base case and a recursive step that repeats until no new rows are added. It solves multi-level hierarchy problems that a single self join cannot.

Want to Build Your Career in Data Analytics with AI?

Join CodeBegun and train with working industry engineers — Check the Data Analyst training details

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 16 July 2026 LinkedIn
Chat with us