Common Table Expressions (CTEs) With WITH
Use a WITH clause to compute total spend per customer as a named, reusable result set, then filter and join against it.
Overview
A Common Table Expression, defined with WITH ... AS (...), lets you give a
name to an intermediate query result and then reference that name later in
the same statement, as if it were a temporary table. This makes multi-step
logic readable: instead of nesting subqueries inside subqueries, you build
up your answer in named stages.
A typical use case is computing a per-customer metric — like total money spent — that requires a join and an aggregation, and then using that computed metric in a further filter or join without repeating the aggregation logic.
The Query
Loading database engine...
Step-by-Step Breakdown
WITH customer_spend AS (...)— defines a named result set. Inside it,ordersis joined toproductsso we can multiplyquantity * priceper line item, then grouped bycustomer_idto get one total per customer.- Once defined,
customer_spendbehaves like any other table for the rest of the statement — you canSELECTfrom it,JOINit, or filter it. SELECT c.name, cs.total_spend FROM customer_spend cs JOIN customers c ON c.id = cs.customer_id— the outer query joins the pre-computed spend totals back tocustomerspurely to get a readable name.ORDER BY cs.total_spend DESC— highest spenders first.
The CTE is evaluated once and referenced by name, which keeps the aggregation logic separate from — and easier to read than — the final join and sort.
Variations
Chain a second CTE that filters down to big spenders only:
Loading database engine...
Each CTE can reference the ones defined before it, separated by commas after
the first WITH. Here, big_spenders reads from customer_spend the same
way the final SELECT would read from a real table.
CTEs aren't automatically materialized
In DuckDB and most modern engines, a CTE isn't necessarily computed once and cached — it may be inlined into the query plan like a view, and re-evaluated if referenced multiple times. Use a CTE for readability and structure; don't rely on it for performance the way you would an actual temp table unless your database specifically documents materialization behavior.
Common Mistakes
- Thinking a CTE persists after the statement. It only exists for the
duration of the single query it's attached to — you can't reference
customer_spendin a later, separate query. - Forgetting the comma between multiple CTEs.
WITH a AS (...) b AS (...)is invalid; it must beWITH a AS (...), b AS (...). - Using a CTE where a simple subquery would be clearer. For a single, small filter, an inline subquery may be easier to read than a named CTE — reach for CTEs when you have multiple steps or want to reuse a result more than once in the same query.
- Assuming CTEs can be referenced recursively without extra syntax.
Self-referencing (recursive) CTEs need an explicit
WITH RECURSIVEdeclaration; a plainWITHCTE can only reference CTEs defined earlier in the same clause, not itself.