| |

SQL 63 🛢️ Common Table Expressions (CTE) with WITH

A Common Table Expression is a named temporary result set that exists for the duration of a single query. It is defined with the WITH keyword, and it can be referenced by name anywhere in the query that follows. The CTE is not a view — it is not stored in the database schema — and it is not a subquery in the traditional sense, because it is defined before the main query and can be referenced multiple times. The feature is part of the SQL standard, and it is supported by PostgreSQL, MySQL 8.0+, SQL Server, Oracle, and SQLite .

The primary value of a CTE is readability. A complex query with nested subqueries is read from the inside out: the innermost subquery is evaluated first, and the outer queries build on it. A CTE is read from the top down: the first CTE is defined, then the second, then the main query. The logic is the same, but the order of reading matches the order of computation. This is why CTEs are often described as “named subqueries that make a query easier to read.” For a query with two or three levels of nesting, the difference is significant .

The secondary value is reuse. A subquery that appears twice in a query is written twice. A CTE is written once and referenced by name. The database may or may not materialize the CTE — it may recompute it for each reference, or it may store the result in a temporary table — but the query is shorter and the logic is defined in one place. In some databases, the MATERIALIZED and NOT MATERIALIZED keywords control this behavior explicitly .

Recursive CTEs are a third capability. A recursive CTE references itself, and the database evaluates it iteratively until no new rows are produced. This is the standard way to query hierarchical data: an organizational chart, a bill of materials, a category tree, or a graph traversal. The recursion has two parts: the anchor member, which is the base case, and the recursive member, which references the CTE by name. The two are combined with UNION ALL or UNION .

This chapter covers three areas. First, why CTEs exist — the readability problem with nested subqueries and the reuse problem with repeated subqueries. Second, how non-recursive CTEs work — the WITH syntax, multiple CTEs, and the interaction with SELECT, INSERT, UPDATE, and DELETE. Third, how recursive CTEs work — the anchor and recursive members, the termination condition, and the standard hierarchical query patterns. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the CTE structure.

Key point: A Common Table Expression is a named temporary result set defined with WITH. It exists only for the duration of the query, and it can be referenced multiple times. A recursive CTE references itself and is evaluated iteratively. CTEs do not create permanent objects and do not change the database schema .


Why CTEs exist

The nested-subquery problem. A query with a subquery in the FROM clause, another subquery in the WHERE clause, and a third in the SELECT list is read from the inside out. The innermost subquery is evaluated first, and the reader must hold each intermediate result in their head while reading the outer query. The nesting is often several levels deep, and the indentation makes the structure hard to follow. A CTE flattens the structure. Each subquery becomes a named block at the top of the query, and the main query references the names. The reading order matches the evaluation order, and the intermediate results are named .

The repeated-subquery problem. A subquery that appears in two places is written twice. If the logic changes, both copies must be updated. If the copies diverge, the query produces incorrect results. A CTE is written once and referenced by name, so there is only one definition. This is the same argument for functions and variables in procedural code: define once, use many times. The database may or may not optimize away the duplication, but the source code no longer contains it .

The hierarchical-data problem. Some data is recursive: an employee table with a manager_id column that references the same table, a category table with a parent_id, a bill of materials with components that are themselves assemblies. Querying this data requires recursion: start with the roots, find their children, find the children of the children, and so on. Before recursive CTEs, this required a stored procedure or application-level iteration. The recursive CTE, introduced in SQL:1999, made the query possible in a single SQL statement. The anchor member defines the base case, and the recursive member defines the step. The database iterates until the recursion produces no new rows .

The readability problem. A query that uses CTEs is easier to read, test, and modify. Each CTE is a named unit that can be understood in isolation. The main query reads like a sequence of steps: “take the first CTE, join it with the second, filter by the third.” The alternative — a query with nested subqueries — reads like a single complex expression. The difference is the same as the difference between a function with named intermediate variables and a function with one giant expression. Both are correct; the named version is easier to debug .

The statement-scope problem. A CTE is not a view. It is not stored in the database schema, and it does not persist beyond the query. It is scoped to the statement that defines it, and it can be referenced only within that statement. This makes it safe to use for temporary transformations that do not need to be reused. A view would persist and could affect other queries; a CTE disappears when the query finishes .

The trade-off. CTEs are not always faster than subqueries. In some databases, a CTE is materialized as a temporary table, and the materialization costs time and memory. In PostgreSQL before version 12, CTEs were always materialized, which could make a query slower than the equivalent subquery. PostgreSQL 12 changed this: CTEs are now inlined by default when they are referenced once, and the MATERIALIZED keyword forces materialization. The trade-off is between the readability of the named blocks and the potential performance cost of materialization. For queries where performance is critical, the execution plan should be examined, and the MATERIALIZED/NOT MATERIALIZED keywords used explicitly .


a. Non-recursive CTEs

A non-recursive CTE is defined with the WITH keyword, followed by a name and a query in parentheses. The main query follows, and it references the CTE by name .

WITH regional_sales AS (
  SELECT region, SUM(amount) AS total_sales
  FROM orders
  GROUP BY region
)
SELECT region, total_sales
FROM regional_sales
WHERE total_sales > 1000;

The CTE regional_sales computes the total sales by region. The main query selects from the CTE and filters by the total. The CTE is a named result set that exists only for this query.

Multiple CTEs are separated by commas:

WITH regional_sales AS (
  SELECT region, SUM(amount) AS total_sales
  FROM orders
  GROUP BY region
),
top_regions AS (
  SELECT region
  FROM regional_sales
  WHERE total_sales > 1000
)
SELECT o.region, o.product, SUM(o.amount) AS product_sales
FROM orders o
JOIN top_regions t ON o.region = t.region
GROUP BY o.region, o.product;

The second CTE references the first. The main query references the second. Each CTE can reference the CTEs defined before it, but not the ones defined after it. This is the dependency order: a CTE is defined before it is used .

A CTE can be used with INSERT, UPDATE, and DELETE:

WITH expensive_products AS (
  SELECT id FROM products WHERE price > 100
)
DELETE FROM order_items
WHERE product_id IN (SELECT id FROM expensive_products);

The CTE is defined, and the DELETE statement references it. The CTE is scoped to the entire statement, so it is available to the modifying clause.

A CTE can also be used with INSERT ... SELECT:

WITH new_orders AS (
  SELECT customer_id, 'pending' AS status FROM customers WHERE active = true
)
INSERT INTO orders (customer_id, status)
SELECT customer_id, status FROM new_orders;

The CTE produces the rows, and the INSERT writes them. This is the standard pattern for inserting derived data .

b. Recursive CTEs

A recursive CTE references itself. The syntax uses WITH RECURSIVE (or just WITH in some databases, where recursion is inferred), and the CTE body has two parts joined by UNION ALL or UNION :

WITH RECURSIVE employee_hierarchy AS (
  -- Anchor member: the roots
  SELECT id, name, manager_id, 1 AS level
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- Recursive member: references the CTE
  SELECT e.id, e.name, e.manager_id, eh.level + 1
  FROM employees e
  JOIN employee_hierarchy eh ON e.manager_id = eh.id
)
SELECT * FROM employee_hierarchy ORDER BY level, name;

The anchor member is the base case. It selects the roots of the hierarchy — the rows where manager_id is NULL. The recursive member references the CTE by name and joins the employees table to it. The join condition e.manager_id = eh.id finds the children of the rows already in the CTE. The level column is incremented at each step. The database evaluates the anchor member first, then the recursive member, then the recursive member again on the new rows, until the recursive member produces no new rows. The result is the full hierarchy with a level for each row .

The termination condition is implicit: the recursion stops when the recursive member produces no new rows. If the data has a cycle — a row that is its own ancestor — the recursion never terminates. The CYCLE clause (SQL:1999, supported in PostgreSQL 14+ and some other databases) detects cycles and stops the recursion. Without it, the query must include a depth limit or a path check .

A recursive CTE can be used for many hierarchical patterns:

Bill of materials:

WITH RECURSIVE bom AS (
  SELECT part_id, component_id, quantity, 1 AS level
  FROM parts
  WHERE part_id = 'A'

  UNION ALL

  SELECT p.part_id, p.component_id, p.quantity, b.level + 1
  FROM parts p
  JOIN bom b ON p.part_id = b.component_id
)
SELECT * FROM bom;

Graph traversal:

WITH RECURSIVE paths AS (
  SELECT source, target, 1 AS depth
  FROM edges
  WHERE source = 'A'

  UNION ALL

  SELECT p.source, e.target, p.depth + 1
  FROM paths p
  JOIN edges e ON p.target = e.source
  WHERE p.depth < 10
)
SELECT * FROM paths;

The WHERE p.depth < 10 clause prevents infinite recursion in a graph with cycles. The depth limit is a common safeguard when the CYCLE clause is not available.

Date series generation:

WITH RECURSIVE dates AS (
  SELECT DATE '2024-01-01' AS day

  UNION ALL

  SELECT day + INTERVAL '1 day'
  FROM dates
  WHERE day < DATE '2024-01-31'
)
SELECT * FROM dates;

The anchor member is the start date. The recursive member adds one day, and the WHERE clause stops at the end date. This is the standard way to generate a series of dates or numbers without a numbers table .

c. Materialization and performance

A CTE can be materialized (computed once and stored in a temporary table) or inlined (substituted into the main query and computed as part of it). The behavior depends on the database and the version .

In PostgreSQL 12 and later, a CTE that is referenced once is inlined by default. A CTE that is referenced multiple times is materialized by default. The MATERIALIZED keyword forces materialization:

WITH expensive_cte AS MATERIALIZED (
  SELECT ... FROM large_table WHERE ...
)
SELECT * FROM expensive_cte;

The NOT MATERIALIZED keyword forces inlining:

WITH simple_cte AS NOT MATERIALIZED (
  SELECT ... FROM small_table
)
SELECT * FROM simple_cte;

Materialization is beneficial when the CTE is expensive to compute and is referenced multiple times, or when the CTE contains a VOLATILE function that must be evaluated once. Inlining is beneficial when the CTE is cheap and the main query can push down filters or joins into it. The default behavior is usually correct, but the keywords give explicit control .

In MySQL 8.0, CTEs are always materialized if they are referenced more than once, and inlined if referenced once. In SQL Server, CTEs are always inlined; they are not materialized unless the query plan chooses to do so. The behavior varies, and the execution plan is the definitive source of truth .

Recursive CTEs are always evaluated iteratively. The anchor member is evaluated once, and the recursive member is evaluated repeatedly until it produces no new rows. The intermediate results are stored in a working table, and each iteration reads from the previous result. This is the standard recursive evaluation strategy, and it is the same in every database that supports recursive CTEs .


Complete Example Session

-- ============================================
-- PART 1: A BASIC CTE
-- ============================================

WITH regional_sales AS (
  SELECT region, SUM(amount) AS total_sales
  FROM orders
  GROUP BY region
)
SELECT region, total_sales
FROM regional_sales
WHERE total_sales > 1000;

-- The CTE computes the total by region.
-- The main query filters the result.


-- ============================================
-- PART 2: MULTIPLE CTEs
-- ============================================

WITH regional_sales AS (
  SELECT region, SUM(amount) AS total_sales
  FROM orders
  GROUP BY region
),
top_regions AS (
  SELECT region
  FROM regional_sales
  WHERE total_sales > 1000
)
SELECT o.region, o.product, SUM(o.amount) AS product_sales
FROM orders o
JOIN top_regions t ON o.region = t.region
GROUP BY o.region, o.product;

-- top_regions references regional_sales.
-- The main query references top_regions.


-- ============================================
-- PART 3: CTE WITH INSERT
-- ============================================

WITH new_orders AS (
  SELECT customer_id, 'pending' AS status
  FROM customers
  WHERE active = true
)
INSERT INTO orders (customer_id, status)
SELECT customer_id, status FROM new_orders;

-- The CTE produces the rows.
-- The INSERT writes them.


-- ============================================
-- PART 4: CTE WITH UPDATE
-- ============================================

WITH expensive_products AS (
  SELECT id FROM products WHERE price > 100
)
UPDATE order_items
SET priority = 'high'
WHERE product_id IN (SELECT id FROM expensive_products);


-- ============================================
-- PART 5: CTE WITH DELETE
-- ============================================

WITH inactive_customers AS (
  SELECT id FROM customers WHERE last_order < NOW() - INTERVAL '1 year'
)
DELETE FROM orders
WHERE customer_id IN (SELECT id FROM inactive_customers);


-- ============================================
-- PART 6: RECURSIVE CTE — EMPLOYEE HIERARCHY
-- ============================================

WITH RECURSIVE employee_hierarchy AS (
  -- Anchor: the roots
  SELECT id, name, manager_id, 1 AS level
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- Recursive: the children
  SELECT e.id, e.name, e.manager_id, eh.level + 1
  FROM employees e
  JOIN employee_hierarchy eh ON e.manager_id = eh.id
)
SELECT id, name, manager_id, level
FROM employee_hierarchy
ORDER BY level, name;

-- The anchor selects the CEO.
-- The recursive member selects direct reports.
-- The recursion continues until no more reports exist.


-- ============================================
-- PART 7: RECURSIVE CTE — BILL OF MATERIALS
-- ============================================

WITH RECURSIVE bom AS (
  SELECT part_id, component_id, quantity, 1 AS level
  FROM parts
  WHERE part_id = 'A'

  UNION ALL

  SELECT p.part_id, p.component_id, p.quantity, b.level + 1
  FROM parts p
  JOIN bom b ON p.part_id = b.component_id
)
SELECT part_id, component_id, quantity, level
FROM bom
ORDER BY level, part_id;


-- ============================================
-- PART 8: RECURSIVE CTE — DATE SERIES
-- ============================================

WITH RECURSIVE dates AS (
  SELECT DATE '2024-01-01' AS day

  UNION ALL

  SELECT day + INTERVAL '1 day'
  FROM dates
  WHERE day < DATE '2024-01-31'
)
SELECT day FROM dates;

-- Generates every day in January 2024.


-- ============================================
-- PART 9: MATERIALIZATION CONTROL
-- ============================================

-- Force materialization
WITH expensive_cte AS MATERIALIZED (
  SELECT ... FROM large_table WHERE ...
)
SELECT * FROM expensive_cte;

-- Force inlining
WITH simple_cte AS NOT MATERIALIZED (
  SELECT ... FROM small_table
)
SELECT * FROM simple_cte;

-- PostgreSQL 12+ inlines CTEs referenced once by default.


-- ============================================
-- PART 10: THE COMPLETE REPORT
-- ============================================

WITH monthly_sales AS (
  SELECT
    DATE_TRUNC('month', order_date) AS month,
    region,
    SUM(amount) AS total
  FROM orders
  GROUP BY month, region
),
running_totals AS (
  SELECT
    month,
    region,
    total,
    SUM(total) OVER (PARTITION BY region ORDER BY month) AS running_total
  FROM monthly_sales
)
SELECT
  month,
  region,
  total,
  running_total,
  ROUND(total / running_total * 100, 2) AS pct_of_running
FROM running_totals
ORDER BY region, month;

-- monthly_sales computes the monthly totals.
-- running_totals adds the running total.
-- The main query computes the percentage.

The ten parts show a basic CTE, multiple CTEs, CTE with INSERT, UPDATE, and DELETE, recursive CTE for hierarchy, bill of materials, date series, materialization control, and the complete report.


Quick Reference

Basic Syntax

FormMeaning
WITH name AS (query) SELECT ...Single CTE
WITH a AS (...), b AS (...) SELECT ...Multiple CTEs
WITH RECURSIVE name AS (...) SELECT ...Recursive CTE
WITH name AS MATERIALIZED (...)Force materialization
WITH name AS NOT MATERIALIZED (...)Force inlining

Recursive CTE Structure

PartMeaning
Anchor memberBase case, no self-reference
UNION ALLCombines anchor and recursive
Recursive memberReferences the CTE by name
TerminationWhen recursive member produces no new rows
CYCLE clauseDetects cycles (PostgreSQL 14+)

Common Patterns

PatternUse case
Multiple CTEsBreak complex query into steps
CTE with INSERTInsert derived data
CTE with UPDATEUpdate based on computed set
CTE with DELETEDelete based on computed set
Recursive hierarchyEmployee, category, org chart
Recursive graphPath traversal, shortest path
Date seriesGenerate missing dates

Database Support

DatabaseCTERecursiveMATERIALIZED
PostgreSQL✅✅✅ (12+)
MySQL✅ (8.0+)✅ (8.0+)Inferred
SQL Server✅✅Inferred
Oracle✅✅Inline hints
SQLite✅ (3.8.3+)✅Inferred

Best Practices

✅ Do This:

-- Name CTEs after what they produce
WITH monthly_totals AS (...)                                              -- ✅
-- Use CTEs to break complex queries into steps
WITH step1 AS (...), step2 AS (...)                                       -- ✅
-- Use recursive CTEs for hierarchical data
WITH RECURSIVE tree AS (...)                                              -- ✅
-- Add a depth limit to prevent infinite recursion
WHERE depth < 10                                                          -- ✅
-- Use MATERIALIZED when the CTE is expensive and referenced multiple times
WITH cte AS MATERIALIZED (...)                                            -- ✅
-- Use CTEs with INSERT/UPDATE/DELETE for derived data
WITH new_rows AS (...) INSERT INTO ... SELECT ... FROM new_rows           -- ✅

❌ Don’t Do This:

-- Don't use a CTE for a one-time simple subquery
WITH x AS (SELECT 1) SELECT * FROM x  -- unnecessary                         -- ❌
-- Don't reference a CTE defined later
WITH a AS (SELECT * FROM b), b AS (...)  -- error: b not defined yet         -- ❌
-- Don't forget the termination condition in recursive CTEs
-- (infinite recursion)                                                     -- ❌
-- Don't assume the CTE is materialized
-- (behavior depends on the database and version)                           -- ❌
-- Don't use CTEs for temporary tables that need to persist
-- (use CREATE TEMPORARY TABLE instead)                                     -- ❌

Common Pitfalls

PitfallWhy It HappensFix
CTE referenced later failsForward referenceDefine CTEs in dependency order
Recursive CTE never terminatesNo termination conditionAdd a depth limit or CYCLE clause
Performance worse than subqueryCTE materialized unnecessarilyTry NOT MATERIALIZED
CTE not visible in subqueryScope is the statementCTE is visible everywhere in the statement
WITH before INSERT syntax errorWrong placementWITH comes before the statement
Duplicate column names in CTENo aliasUse explicit aliases
Recursive CTE with UNION instead of UNION ALLDeduplication overheadUse UNION ALL unless dedup is needed

Real-World Examples

1. Basic CTE

WITH regional_sales AS (SELECT region, SUM(amount) FROM orders GROUP BY region)
SELECT * FROM regional_sales;

2. Multiple CTEs

WITH a AS (...), b AS (...)
SELECT * FROM a JOIN b ON ...;

3. CTE with INSERT

WITH new_rows AS (...) INSERT INTO target SELECT * FROM new_rows;

4. CTE with UPDATE

WITH computed AS (...) UPDATE target SET ... WHERE id IN (SELECT id FROM computed);

5. Recursive Hierarchy

WITH RECURSIVE tree AS (
  SELECT id, parent_id, 1 FROM nodes WHERE parent_id IS NULL
  UNION ALL
  SELECT n.id, n.parent_id, t.level + 1 FROM nodes n JOIN tree t ON n.parent_id = t.id
)
SELECT * FROM tree;

6. Date Series

WITH RECURSIVE dates AS (
  SELECT DATE '2024-01-01' AS d
  UNION ALL
  SELECT d + 1 FROM dates WHERE d < DATE '2024-01-31'
)
SELECT * FROM dates;

7. Bill of Materials

WITH RECURSIVE bom AS (
  SELECT part_id, component_id, 1 FROM parts WHERE part_id = 'A'
  UNION ALL
  SELECT p.part_id, p.component_id, b.level + 1 FROM parts p JOIN bom b ON p.part_id = b.component_id
)
SELECT * FROM bom;

8. Graph Traversal

WITH RECURSIVE paths AS (
  SELECT source, target, 1 FROM edges WHERE source = 'A'
  UNION ALL
  SELECT p.source, e.target, p.depth + 1 FROM paths p JOIN edges e ON p.target = e.source WHERE p.depth < 10
)
SELECT * FROM paths;

9. Materialization Control

WITH cte AS MATERIALIZED (...) SELECT * FROM cte;

10. CTE with Window Function

WITH monthly AS (...), running AS (SELECT *, SUM(total) OVER (...) FROM monthly)
SELECT * FROM running;

Visual

CTE Structure

┌──────────────────────────────────────────────────────────────┐
│  CTE STRUCTURE                                               │
│                                                              │
│  WITH cte_name AS (                                          │
│    SELECT ...                                                │
│    FROM ...                                                  │
│    WHERE ...                                                 │
│  )                                                           │
│  SELECT ...                                                  │
│  FROM cte_name                                               │
│  WHERE ...;                                                  │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  WITH clause          Main query                     │    │
│  │  ┌──────────────┐    ┌──────────────────────┐        │    │
│  │  │  cte_name    │    │  SELECT ...          │        │    │
│  │  │  (query)     │───►│  FROM cte_name       │        │    │
│  │  └──────────────┘    └──────────────────────┘        │    │
│  │                                                       │    │
│  │  The CTE is defined first, then referenced.          │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  Reading order = evaluation order.                           │
│  The CTE is a named temporary result set.                    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Multiple CTEs

┌──────────────────────────────────────────────────────────────┐
│  MULTIPLE CTEs                                               │
│                                                              │
│  WITH a AS (                                                 │
│    SELECT ... FROM base                                      │
│  ),                                                          │
│  b AS (                                                      │
│    SELECT ... FROM a                                         │
│  ),                                                          │
│  c AS (                                                      │
│    SELECT ... FROM a JOIN b ON ...                           │
│  )                                                           │
│  SELECT ... FROM c;                                          │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  base                                                │    │
│  │    │                                                 │    │
│  │    ▼                                                 │    │
│  │  a  ──────────┐                                      │    │
│  │    │          │                                      │    │
│  │    ▼          ▼                                      │    │
│  │  b          c (joins a and b)                        │    │
│  │               │                                      │    │
│  │               ▼                                      │    │
│  │            main query                                │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  Each CTE can reference the CTEs defined before it.          │
│  The dependency order is top to bottom.                      │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Recursive CTE

┌──────────────────────────────────────────────────────────────┐
│  RECURSIVE CTE                                               │
│                                                              │
│  WITH RECURSIVE tree AS (                                    │
│    SELECT id, parent_id, 1 AS level   ← ANCHOR               │
│    FROM nodes                                                │
│    WHERE parent_id IS NULL                                   │
│                                                              │
│    UNION ALL                                                 │
│                                                              │
│    SELECT n.id, n.parent_id, t.level + 1  ← RECURSIVE        │
│    FROM nodes n                                              │
│    JOIN tree t ON n.parent_id = t.id                         │
│  )                                                           │
│  SELECT * FROM tree;                                         │
│                                                              │
│  Evaluation:                                                 │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  Iteration 1: anchor → roots (level 1)               │    │
│  │  Iteration 2: recursive on level 1 → level 2         │    │
│  │  Iteration 3: recursive on level 2 → level 3         │    │
│  │  ...                                                  │    │
│  │  Stops when recursive member produces no new rows    │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  Anchor = base case.                                         │
│  Recursive = step.                                           │
│  UNION ALL combines them.                                    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

CTE vs Subquery

┌──────────────────────────────────────────────────────────────┐
│  CTE vs SUBQUERY                                             │
│                                                              │
│  NESTED SUBQUERY:                                            │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  SELECT ...                                          │    │
│  │  FROM (                                              │    │
│  │    SELECT ...                                        │    │
│  │    FROM (                                            │    │
│  │      SELECT ...                                      │    │
│  │      FROM base                                       │    │
│  │      WHERE ...                                       │    │
│  │    ) AS inner                                        │    │
│  │    WHERE ...                                         │    │
│  │  ) AS outer                                          │    │
│  │  WHERE ...;                                          │    │
│  │                                                       │    │
│  │  Read from inside out.                               │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  CTE:                                                        │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  WITH inner AS (                                     │    │
│  │    SELECT ... FROM base WHERE ...                    │    │
│  │  ),                                                  │    │
│  │  outer AS (                                          │    │
│  │    SELECT ... FROM inner WHERE ...                   │    │
│  │  )                                                   │    │
│  │  SELECT ... FROM outer WHERE ...;                    │    │
│  │                                                       │    │
│  │  Read from top to bottom.                            │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
CTENamed temporary result set defined with WITH
ScopeThe single statement that defines it
Multiple CTEsSeparated by commas; each can reference earlier ones
Recursive CTEWITH RECURSIVE, anchor + recursive members
TerminationRecursive member produces no new rows
MATERIALIZEDForces the CTE to be computed once
NOT MATERIALIZEDForces the CTE to be inlined
Use withSELECT, INSERT, UPDATE, DELETE
PersistenceNone; disappears when the query finishes

Key takeaways:

  • A CTE is a named temporary result set. It is defined with WITH, and it exists only for the duration of the statement that defines it. It is not stored in the database schema, and it does not persist beyond the query .
  • CTEs improve readability. A query with nested subqueries is read from the inside out. A query with CTEs is read from the top down, and the reading order matches the evaluation order. Each CTE is a named unit that can be understood in isolation .
  • Multiple CTEs are separated by commas. Each CTE can reference the CTEs defined before it, but not the ones defined after it. The dependency order is top to bottom .
  • CTEs can be used with INSERT, UPDATE, and DELETE. The WITH clause comes before the statement, and the CTE is visible throughout the statement. This is the standard pattern for inserting, updating, or deleting derived data .
  • A recursive CTE references itself. The anchor member is the base case, and the recursive member references the CTE by name. The two are combined with UNION ALL. The recursion terminates when the recursive member produces no new rows .
  • Recursive CTEs require a termination condition. Without one, a cycle in the data causes infinite recursion. The CYCLE clause (PostgreSQL 14+) detects cycles, and a depth limit is the common safeguard when the clause is not available .
  • Materialization behavior varies by database and version. PostgreSQL 12+ inlines CTEs referenced once and materializes CTEs referenced multiple times. The MATERIALIZED and NOT MATERIALIZED keywords give explicit control. The execution plan is the definitive source of truth .
  • CTEs are not always faster than subqueries. A materialized CTE can be slower than an inlined subquery. The performance depends on the query, the database, and the version. For critical queries, examine the execution plan and choose the materialization strategy explicitly .

Remember: A Common Table Expression is a named temporary result set defined with WITH. It makes complex queries easier to read by replacing nested subqueries with named blocks, and it makes repeated logic easier to maintain by defining it once. Recursive CTEs extend the feature to hierarchical and graph data, with an anchor member for the base case and a recursive member for the step. The CTE exists only for the duration of the statement, and it does not persist. Materialization behavior varies by database, and the MATERIALIZED/NOT MATERIALIZED keywords give explicit control. For analytical queries with multiple steps, CTEs are the standard tool for organizing the logic.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!