skip to content

Common Table Expressions

The WITH clause: naming intermediate result sets so a complex statement reads top-down, plus the recursive form that lets plain SQL walk trees and graphs. CTE questions test whether you can structure a multi-step query and handle hierarchies without procedural code.

part ofSQLoverview, primer and where to startread it →
on this pageshow

questions

19

What is the difference between a CTE, a derived table, and a view?

level: juniorimportance: must knowfreq 70%

answer

  1. Three ways to name a subresult
  2. The difference is how far the name reaches
  3. One block, one statement, or the whole database
  4. Only one of the three is stored in the catalog

basics

~20 s

All three name a subresult. A derived table is a subquery written inline in FROM and used once there; a CTE is named in a WITH clause and visible only to that one statement; a view is a stored schema object any statement can query.

solid answer

~60 s

All three let a query treat a subresult as if it were a table; they differ in **scope and lifetime**. A **derived table** is a subquery inside `FROM`, given a correlation name: `FROM (SELECT ...) AS d`. It exists only inside the query block that contains it, and if another part of the query needs the same result you write the subquery out again. A **CTE** is named in a `WITH` clause attached to a statement: `WITH d AS (SELECT ...) SELECT ...`. The name is available anywhere in that statement — including several times, or in a self-join — and vanishes when the statement ends. Nothing is stored. A **view** is created with `CREATE VIEW` and lives in the catalog, so any later statement, session or application can select from it. It is a schema object, so it belongs to your migrations. Rule of thumb: once in one block → derived table; more than once in one statement, or to make nesting readable → CTE; needed by more than one statement → view.

code

sql · 19 lines
sql
-- 1. Derived table: inline in FROM, correlation name required
SELECT d.dept_id, d.headcount
FROM (SELECT dept_id, COUNT(*) AS headcount
      FROM employee GROUP BY dept_id) AS d
WHERE d.headcount > 10;

-- 2. CTE: named in WITH, visible only inside this statement
WITH dept_size AS (
    SELECT dept_id, COUNT(*) AS headcount
    FROM employee GROUP BY dept_id
)
SELECT dept_id, headcount FROM dept_size WHERE headcount > 10;

-- 3. View: stored in the catalog, any later statement can use it
CREATE VIEW dept_size AS
    SELECT dept_id, COUNT(*) AS headcount
    FROM employee GROUP BY dept_id;

SELECT dept_id, headcount FROM dept_size WHERE headcount > 10;

go deeper

for a junior

Be ready to define all three in one sentence each and to point at which is which in a query you are shown. Knowing that a view persists and the other two do not is the minimum expected answer.

for a middle

Explain the scoping rules precisely: which parts of a statement can see a derived table's alias, that a CTE name covers the whole statement and dies with it, and that only a view is reachable from other statements.

for a senior

Show judgment about maintenance, not just syntax: when a repeated subquery deserves promotion, when a view would become a shared dependency nobody can change, and how naming the subresult affects the next person to read the query.

for a principal

Own the convention: what your team standardises on for report queries, how many stored views the schema should carry, and how you stop one-off logic from silently becoming a schema object other teams build on.

## What all three have in common A derived table, a common table expression (CTE) and a view all do the same basic job: they give a query result a name so that a bigger query can use it as though it were a table. In all three cases the thing being named is a `SELECT` statement, and in all three cases the outer query can join to it, filter it, group it and select columns from it exactly as it would from a base table. Because they look interchangeable in a query's `FROM` clause, interviewers ask candidates to state what actually separates them. The separating axis is **scope**: how far the name reaches, and how long it lives. ## Derived table A derived table is a subquery written directly in the `FROM` clause: ```sql SELECT d.dept_id, d.headcount FROM (SELECT dept_id, COUNT(*) AS headcount FROM employee GROUP BY dept_id) AS d WHERE d.headcount > 10; ``` Standard SQL requires the derived table to be given a correlation name — the alias `d` — and engines differ only in how strictly they enforce that. The alias is visible to the query block that contains the `FROM` item: that block's join conditions, `WHERE`, `GROUP BY`, `HAVING` and select list. It is not visible anywhere else. If a second `FROM` item, or a subquery elsewhere in the statement, needs the same result, you must write the subquery text out a second time. Nothing survives the statement; there is no catalog entry and no name to reuse. ## Common table expression A CTE is a subquery given a name in a `WITH` clause attached to a single statement: ```sql WITH dept_size AS ( SELECT dept_id, COUNT(*) AS headcount FROM employee GROUP BY dept_id ) SELECT dept_id, headcount FROM dept_size WHERE headcount > 10; ``` `WITH` has been in the SQL standard since SQL:1999. Several CTEs can be listed, separated by commas, and each may reference the ones defined before it — which is why stacked CTEs read like a pipeline of named steps. An optional column list renames the output: `WITH dept_size (dept_id, headcount) AS (...)`. With `WITH RECURSIVE`, a CTE may also reference itself. The important property for this comparison is that the CTE name is visible to the **whole statement** the `WITH` is attached to. Every `FROM` in the main query can use it, including more than once and including a self-join of the CTE to itself. When the statement finishes, the name is gone: the next statement in the same transaction or session cannot select from it. Nothing is written to the catalog, and no privileges or migrations are involved — a CTE is just part of the text of one statement. ## View A view stores the query in the database under a name: ```sql CREATE VIEW dept_size AS SELECT dept_id, COUNT(*) AS headcount FROM employee GROUP BY dept_id; ``` From then on, any statement — in any session, from any application, written by anyone with access — can say `FROM dept_size`. That is the whole point of a view and the one thing neither of the other two constructs can do. The cost of that reach is that a view is a schema object: it is created and dropped with DDL, so it belongs in your migration scripts, and once other queries reference it, changing its shape becomes a coordinated change rather than an edit to one file. A view also takes no parameters; callers narrow it by adding their own predicates against it in their own `WHERE`. ## Choosing between them - Needed once, inside one query block → **derived table**. It keeps the definition next to its only use. - Needed more than once in one statement, or the nesting has become hard to follow → **CTE**. One definition, one name, referenced as often as you like. - Needed by more than one statement, report or team → **view**. One definition in the database instead of copies in several files. Naming is an underrated part of the choice. `WITH active_customer AS (...)` tells the next reader what the subresult *means*; a nested `(SELECT ...) AS t3` does not. ## What the comparison is not about Candidates often jump straight to "a CTE is materialized and a derived table is inlined." Whether an engine computes a subresult once, folds it into the surrounding query, or re-evaluates it per reference is an engine-level decision that varies by engine and by version, and it is not what the language guarantees. At the language level the guarantee is about naming and scope: a CTE gives you one definition reachable from the whole statement, a derived table gives you a definition reachable from one block, and a view gives you a definition reachable from every statement. ## Common mistakes Forgetting the alias on a derived table; expecting a CTE name to still exist in the next statement; creating a view for a subresult only one query will ever want, which adds a schema object nobody can safely delete later; and assuming that because the three read alike in `FROM`, the choice does not matter to the people who maintain the query afterwards.

  • Can a second FROM item in the same query select from a derived table defined earlier in that FROM clause?
    No. A derived table's correlation name is usable inside the query block that contains it — its join conditions, WHERE, GROUP BY and select list — but you cannot start another table reference that reads from it by name. That is exactly the limitation a CTE removes: a CTE name is available to every FROM in the statement.
  • Does a CTE name survive to the next statement in the same transaction?
    No. A CTE belongs to the single statement its WITH clause is attached to. When that statement ends the name is gone, whether or not the transaction is still open. If a second statement needs the same definition you either repeat it or promote it to a stored object such as a view.
  • Can a view's defining query itself contain a WITH clause?
    In engines that support both features this is generally allowed, and it is a normal way to keep a complex view readable: the WITH stack is part of the stored query text. Support is an engine detail, so confirm it in your engine's documentation before relying on it.

A derived table is a note scribbled in the margin of one paragraph, a CTE is a definition at the top of the page that the whole page may use, and a view is an entry in the dictionary everyone shares.

saying these in an interview costs you the question

  • Says a CTE is stored in the database like a view
  • Claims a CTE is always materialized and a derived table never is
  • Thinks a CTE name is reusable by later statements in the session
  • Forgets a derived table needs a correlation name
  • Says the three are interchangeable, so the choice does not matter

context

open as a page

What does a WITH clause define in SQL, and how long does the named result live?

level: juniorimportance: must knowfreq 72%

basics

~20 s

WITH defines one or more named subqueries, called common table expressions, that the statement following it can reference by name as if they were tables. The names exist only for that single statement and are never stored.

open as a page

How do you add a depth column to a recursive CTE that walks a manager_id chain?

level: middleimportance: must knowfreq 70%

basics

~20 s

Initialise the depth in the anchor member (0 or 1), then select depth + 1 in the recursive member. Each new row inherits its parent's depth plus one, so the column measures the distance in links from the starting node.

open as a page

In a WITH RECURSIVE query, what does each iteration read, and when does iteration stop?

level: middleimportance: must knowfreq 65%

basics

~20 s

Each pass of the recursive member sees only the rows the previous pass produced, not the whole accumulated result. Its output is appended to the result and becomes the input for the next pass. Iteration stops when a pass produces no rows.

open as a page

How do you stop a recursive CTE from looping forever when the hierarchy contains a cycle?

level: seniorimportance: must knowfreq 60%

basics

~20 s

A recursive CTE has no built-in loop protection. Carry the visited node ids in a path column and add a predicate in the recursive member that skips any node already there. Where implemented, the standard CYCLE clause does this for you.

open as a page

A recursive CTE never finishes and has to be killed — how do you make it terminate?

level: seniorimportance: must knowfreq 50%

basics

~20 s

Recursion ends only when a pass produces no rows, so an unbounded query means the recursive member always finds something. The reliable fix is a depth counter incremented each pass and filtered inside the recursive member, which bounds work regardless of the data.

open as a page

In a recursive CTE over (id, parent_id), what changes when you walk ancestors instead of descendants?

level: juniorimportance: should knowfreq 50%

basics

~20 s

The join direction flips. For descendants you join the table's parent_id to the CTE's id; for ancestors you join the table's id to the CTE's parent_id. The anchor — the node you start from — is written the same way in both.

open as a page

What are the two members of a WITH RECURSIVE query, and how do they combine?

level: juniorimportance: should knowfreq 45%

basics

~20 s

A WITH RECURSIVE query has an anchor member that runs once to seed the result and does not mention the CTE's name, plus a recursive member that does reference that name and runs repeatedly. UNION ALL or UNION joins them.

open as a page

Why prefer a CTE over repeating a derived table when one query needs the same subresult twice?

level: middleimportance: should knowfreq 52%

basics

~20 s

A derived table's alias reaches only the block it sits in, so a second use means pasting the subquery again and keeping two copies in sync. A CTE is written once, named, and referenced as often as the statement needs.

open as a page

How do you build a breadcrumb path column in a recursive CTE over a category tree?

level: middleimportance: should knowfreq 55%

basics

~20 s

Concatenate in the recursive member: the parent row's accumulated path, a delimiter, then the current row's name. Initialise the path in the anchor with an explicit CAST to a wide string type, and ORDER BY path to print the tree depth-first.

open as a page

In WITH RECURSIVE, what changes if the members are combined with UNION instead of UNION ALL?

level: middleimportance: should knowfreq 42%

basics

~20 s

UNION discards rows that duplicate rows already produced, so a repeat row is never fed into the next pass and the iteration ends once nothing new appears. UNION ALL keeps every row and needs its own stop predicate.

open as a page

Can a CTE in a WITH clause reference another CTE, and does definition order matter?

level: middleimportance: should knowfreq 58%

basics

~20 s

Yes. A CTE may reference any CTE defined earlier in the same WITH list, which is how multi-step queries are stacked. Order matters: referring to a name defined later fails unless the CTE is recursive.

open as a page

How do you use a WITH clause with a DELETE or UPDATE, and what can the CTE see?

level: middleimportance: should knowfreq 38%

basics

~20 s

Write the WITH clause in front of the DELETE or UPDATE; the CTE names are then usable in that statement's subqueries and predicates. The names vanish when the statement ends, and engine support for WITH before DML varies, so check yours.

open as a page

When is a CTE that several reports now duplicate worth promoting to a view?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Promote when more than one statement needs the same definition and that definition is stable and meaningful on its own. A CTE dies with its statement, so every other query must copy it; a view keeps one definition everyone reads.

open as a page

Using a recursive CTE over a multi-level bill of materials, how do you total each component's required quantity?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Multiply quantities down each path inside the recursive member — the child's quantity times the parent row's accumulated quantity — then GROUP BY component and SUM in the outer query, so a component reached by several paths is totalled correctly.

open as a page

How would you refactor a query with four levels of nested subqueries into stacked CTEs?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Work from the innermost subquery outward: lift each level into its own named CTE in the WITH list, in dependency order, each selecting from the previous one. The result set must be unchanged; only the shape and the names are new.

open as a page

If a WITH clause defines a name that also exists as a table, which one does the query read?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

The CTE wins inside that statement: references in the main query resolve to the WITH definition, not the base table or view of the same name. The shadowing ends with the statement, so the next statement sees the schema object again.

open as a page

What is not allowed inside the recursive member of a WITH RECURSIVE query?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

The recursive member may reference the CTE's own name only once, in its FROM clause, and not inside a subquery. Aggregates, window functions, GROUP BY, HAVING and DISTINCT over the recursive reference are rejected, as is putting it on the null-supplying side of an outer join.

open as a page

What does the optional column list in WITH totals (mth, revenue) AS (...) do?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

It renames the CTE's output columns positionally, so the CTE exposes those names instead of whatever the inner SELECT produced. The list must name exactly as many columns as the query returns, or the statement fails.

open as a page