← Back to list

Everything You Need to Know About CTEs in SQL

Have you ever written a big SQL query with multiple joins, window functions, and subqueries — and felt like the whole thing looked like a…

Saswati S · 2026-06-06 07:36 · 32 claps · 4.8 min read
#sql #cte #tech #database #common-table-expressions
Open on Medium ↗
Wiki topics: 🌐 · Web Development 🔧 · Data Engineering 📰 · Journalism & News

Everything You Need to Know About CTEs in SQL

Have you ever written a big SQL query with multiple joins, window functions, and subqueries — and felt like the whole thing looked like a wall of code? When an error pops up, you don’t even know where to start looking.

Your fix is a CTE — Common Table Expression.

What Is a CTE?

A CTE is a temporary, named result set — kind of like a virtual table — that can be used multiple times within a query to simplify and organise your logic. But it only exists for the duration of that query. No permanent storage. No creating actual tables. Just a clean, readable way to break a complex query into named steps.

Syntax

WITH cte_name AS (
    -- your query goes here
    SELECT ...
    FROM ...
    WHERE ...
)
SELECT *
FROM cte_name;

The WITH keyword starts the CTE. You give it a name. You write your query inside the brackets. Then you use that name in the main query below — just like a real table.

Advantages of CTEs

  • Improves readability — your query tells a story instead of looking like a puzzle
  • Modularity — breaks complex logic into small, named chunks
  • Reusability — can be referenced multiple times within the same query

CTE vs Subquery — When to Use Which

Situation Use Logic used only once, simple query Subquery is fine Same logic needed multiple times CTE Query has many levels of nesting CTE You want readable, maintainable SQL CTE Quick inline filter Subquery Recursive logic (hierarchy, tree) CTE (recursive)

One rule worth remembering: if your subquery is more than 5 lines or appears more than once, turn it into a CTE.

CTE vs Temp Table vs View

These three often get confused. Here’s how they differ:

                  CTE           Temp Table        View
─────────────────────────────────────────────────────────
Scope           Single query  Session-wide      Permanent
Stored on disk  No            Yes               No (just SQL)
Reusable        No            Yes (in session)  Yes (always)
Recursive       Yes           No                No
Performance     Same as SQ    Faster for large  Depends
                              repeated queries
Best for        Readability,  Heavy repeated    Shared, reused
                one query     computation       logic across queries
  • Use a CTE when you need clean, readable logic within one query.
  • Use a Temp Table when you’re doing heavy computation that you’ll query multiple times in a session — the result gets stored on disk.
  • Use a View when you want to save a query that multiple people or queries will reuse permanently.

Real-World Use Case — Running Totals

Calculate a running total of sales by date:

WITH daily_sales AS (
    SELECT
        order_date,
        SUM(amount) AS daily_total
    FROM orders
    GROUP BY order_date
)
SELECT
    order_date,
    daily_total,
    SUM(daily_total) OVER (ORDER BY order_date) AS running_total
FROM daily_sales
ORDER BY order_date;

The CTE first aggregates sales by day. The main query then applies a window function on top of that clean result — much easier than trying to do both in one go.

Types of CTEs:

Recursive CTEs — The Advanced Part

A recursive CTE is one that references itself. This sounds confusing at first, but it solves one specific type of problem perfectly: hierarchical data.

Think of an org chart, a file system, a category tree, or a bill of materials. These are all cases where rows refer to other rows in the same table.

Example: Employee → Manager hierarchy

-- Table structure:
-- emp_id | name     | manager_id
-- 1      | CEO      | NULL
-- 2      | CTO      | 1
-- 3      | Manager  | 2
-- 4      | Dev      | 3

Find the full reporting chain from any employee up to the CEO:

WITH RECURSIVE org_chart AS (
-- Anchor: start with the CEO (no manager)
    SELECT emp_id, name, manager_id, 0 AS level
    FROM employees
    WHERE manager_id IS NULL
    UNION ALL
    -- Recursive: keep joining to find the next level down
    SELECT e.emp_id, e.name, e.manager_id, oc.level + 1
    FROM employees e
    JOIN org_chart oc ON e.manager_id = oc.emp_id
)
SELECT emp_id, name, level
FROM org_chart
ORDER BY level;

Output:

emp_id  name      level
1       CEO       0
2       CTO       1
3       Manager   2
4       Dev       3

A recursive CTE always has two parts:

WITH RECURSIVE cte_name AS (
-- 1. Anchor member (starting point, runs once)
    SELECT ...
    UNION ALL
    -- 2. Recursive member (references the CTE itself, runs repeatedly)
    SELECT ...
    FROM cte_name  -- ← this is what makes it recursive
    JOIN ...
)

The database runs the anchor first, gets a result, then runs the recursive part on that result, then runs it again on the new result — and keeps going until no new rows are produced. Then it combines everything with UNION ALL.

Another Example: Generate numbers from 1 to 20

WITH RECURSIVE generate_num AS (
    SELECT 1 AS num
    UNION ALL
    SELECT num + 1
    FROM generate_num
    WHERE num < 20
)
SELECT * FROM generate_num;

Why num < 20 and not num <= 20?

This is a subtle but important point. Remember — the WHERE condition controls when the recursion stops, not what gets included in the final result.

Here’s what happens step by step:

Anchor runs first  → produces num = 1
Recursive runs     → WHERE 1 < 20  ✅ → produces num = 2
Recursive runs     → WHERE 2 < 20  ✅ → produces num = 3
...
Recursive runs     → WHERE 19 < 20 ✅ → produces num = 20
Recursive runs     → WHERE 20 < 20 ❌ → stops here

So WHERE num < 20 allows the recursive step to run when num is 19, which generates 20 — and then stops. Your final result includes 1 through 20. ✅

Now if you used num <= 20:

Recursive runs     → WHERE 19 <= 20 ✅ → produces num = 20
Recursive runs     → WHERE 20 <= 20 ✅ → produces num = 21  ← one extra!
Recursive runs     → WHERE 21 <= 20 ❌ → stops here

You’d get 1 through 21 — one number more than you wanted. ❌

The rule: the condition WHERE num < N means "keep going while num is less than N" — so the last value generated is N itself. Think of it like a for loop:

# This prints 1 to 20, not 1 to 19
for num in range(1, 21):   # stop BEFORE 21
    print(num)

Same logic. The condition is the exit gate — when it becomes false, the recursion stops and that row is not added.

When to use recursive CTEs:

  • Org charts / employee hierarchy
  • Category trees (parent → child → grandchild)
  • File system paths
  • Bill of materials (product → components → sub-components)
  • Finding connected nodes in a graph

One Last Thing

CTEs are not a performance hack. They’re a thinking tool.

The real value is that they force you to break a problem into named, logical steps — and named things are easier to debug, easier to explain, and easier to change six months later when you’ve forgotten what you wrote.

Write SQL that the next person (or future you) can read without needing a decoder ring. CTEs are how you do that.


메타데이터
post_id
82bed206bd10
slug
everything-you-need-to-know-about-ctes-in-sql-82bed206bd10
url
https://medium.com/@saswativirat18/everything-you-need-to-know-about-ctes-in-sql-82bed206bd10
canonical_url
https://medium.com/@saswativirat18/everything-you-need-to-know-about-ctes-in-sql-82bed206bd10
author_url
https://medium.com/@saswativirat18
status
ok
fetched_at
2026-06-10 21:21:38