WITH CTE as SQL

Learn with CTE as SQL in 2026 with our practical guide. Real examples show you how to use Common Table Expressions for cleaner queries.

By Draxlr TeamPublished on 30 Jul 2026
WITH CTE as SQL

You inherit the query at 4:55 p.m. The dashboard is slow, the alert is noisy, and nobody wants to touch the SQL because it looks like a stack of nested brackets built by three different people over two quarters. That's exactly where CTEs, or Common Table Expressions, earn their keep, because they turn one hard-to-read statement into a sequence of named steps that you can reason about without rewriting the schema.

Table of Contents

Why CTEs Make SQL Easier to Read and Maintain

The kind of SQL commonly inherited is hard to trust because it's hard to scan. You see a five-level nested subquery, a couple of repeated joins, and a filter buried so deep that changing one condition feels like a risk to the whole statement. A CTE with WITH ... AS is the practical escape hatch, because it lets you name each intermediate result and turn bracket hell into something that reads like a sequence of business steps.

Before and after

A nested query often looks like this in spirit, even if the exact shape varies by engine:

SELECT *
FROM (
  SELECT customer_id, SUM(order_total) AS revenue
  FROM orders
  WHERE status = 'paid'
  GROUP BY customer_id
) r
JOIN customers c ON c.id = r.customer_id
WHERE r.revenue > 1000;

The same logic with a CTE is easier to follow:

WITH customer_revenue AS (
  SELECT customer_id, SUM(order_total) AS revenue
  FROM orders
  WHERE status = 'paid'
  GROUP BY customer_id
)
SELECT c.id, c.name, cr.revenue
FROM customer_revenue cr
JOIN customers c ON c.id = cr.customer_id
WHERE cr.revenue > 1000;

The second version doesn't just look cleaner. It gives the next person a place to understand the logic one step at a time, which matters when a BI dashboard breaks and someone has to debug it under pressure.

Practical rule: If a query has a repeated subquery or a chain of transformations, name each step first. Readability usually drops before logic quality does.

Why the scope rule matters

A CTE only exists for the single statement that follows it. Microsoft documents that scope clearly in Transact-SQL, and MySQL 8.0 documents the same WITH pattern for temporary named result sets that live only for that statement Microsoft CTE documentation, MySQL WITH clause documentation. That means you get cleaner SQL without creating a persistent object, and you don't end up managing cleanup with DROP statements or schema clutter.

That temporary scope is also why CTEs fit well in live analytics systems. They let you isolate a transformation, validate it, and keep moving without leaving behind tables that nobody remembers to remove.

CTE Syntax and Common Patterns

A comprehensive infographic explaining SQL Common Table Expressions syntax, patterns, and best practices for better code.

The canonical form is simple, and that is part of why it stays useful. You define the temporary result set with WITH, give it a name, then use it in the statement that follows. A concrete example against orders and customers usually makes the pattern easier to grasp than abstract syntax notes.

The basic pattern

WITH paid_orders AS (
  SELECT customer_id, order_id, order_total
  FROM orders
  WHERE status = 'paid'
)
SELECT c.name, p.order_id, p.order_total
FROM paid_orders p
JOIN customers c ON c.id = p.customer_id;

That example matches the standard shape shown in SQL walkthroughs, where a CTE is a named result set that can be referenced like a table inside the same statement CTE example and syntax reference, canonical WITH cte_name AS (SELECT ...) pattern.

Common variations

Sometimes you want to be explicit about column names, especially when the select list is long or derived:

WITH paid_orders (customer_id, order_id, revenue) AS (
  SELECT customer_id, order_id, order_total
  FROM orders
  WHERE status = 'paid'
)
SELECT *
FROM paid_orders;

You can also chain multiple CTEs under one WITH keyword, separated by commas. Analysts use this pattern when one transformation feeds the next.

WITH paid_orders AS (
  SELECT customer_id, order_id, order_total
  FROM orders
  WHERE status = 'paid'
),
customer_spend AS (
  SELECT customer_id, SUM(order_total) AS revenue
  FROM paid_orders
  GROUP BY customer_id
)
SELECT *
FROM customer_spend;

If you are writing the logic in a workflow tool or a SQL editor, that layered structure is easier to audit than a single dense SELECT. Devart also notes that multiple CTEs can be chained this way, and that a CTE can be referenced like a table within the same statement multi-CTE chaining and table-like reference.

A CTE can also be used with INSERT, UPDATE, DELETE, and MERGE in engines that support those statement types. Microsoft's documentation explicitly includes those consuming statements in scope Microsoft CTE documentation.

For a deeper syntax walk-through in PostgreSQL terms, this PostgreSQL CTE example guide is a useful companion if you want to compare the shape against a familiar production engine.

The main point is that the syntax stays stable across major SQL systems, but the value comes from naming intermediate logic cleanly. Once you can write one CTE, chaining them becomes a matter of organizing the business question into stages instead of forcing everything into one block.

Writing Recursive CTEs Without Exploding Your Query

Recursive CTEs solve a different problem from the basic WITH pattern. They're for hierarchies, paths, and repeated expansion, which is why they show up in org charts, category trees, and date-series generation. The mechanics are simple enough to write, but they're also where a small mistake can turn into a large result explosion.

Anchor member and recursive member

A recursive CTE usually has two parts. The anchor member seeds the result set, and the recursive member references the CTE itself to keep expanding until no new rows appear. Educational walkthroughs describe this as a loop that re-runs the recursive part until the termination condition fails recursive CTE flow and termination concept.

A common employee hierarchy pattern looks like this:

WITH RECURSIVE org_tree AS (
  SELECT employee_id, manager_id, employee_name, 0 AS level
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  SELECT e.employee_id, e.manager_id, e.employee_name, ot.level + 1
  FROM employees e
  JOIN org_tree ot ON e.manager_id = ot.employee_id
)
SELECT *
FROM org_tree;

The first SELECT finds the top of the tree. The second SELECT keeps finding direct reports until there are no more children to add.

The part that breaks queries

Recursive CTEs fail when the termination logic is weak, missing, or pointed at the wrong column. If the recursive member keeps finding the same rows, or keeps expanding a branch you didn't intend, the query can run far longer than expected or produce a huge intermediate set.

Practical rule: Validate the base case first, then test the recursive filter against a small slice of data before you trust it in production.

A short checklist helps:

  • Confirm the seed row set: Make sure the anchor member starts where you think it does.
  • Check the join path: Confirm the recursive member walks the hierarchy in the right direction.
  • Add a recursion cap where supported: SQL Server supports MAXRECURSION, which is useful as a safety net.
  • Test on a small tree first: A small org chart or category set catches logic errors early.

Recursive CTEs are the right tool for parent-child structures, dependency trees, nested folders, and date generation. A self-join is often simpler for one or two levels, but once you need repeated traversal, the recursive pattern is the cleaner and safer expression.

An infographic illustrating how to write safe recursive SQL CTEs without causing system performance issues.

How Major SQL Engines Execute a CTE

The same WITH ... AS syntax does not guarantee the same execution behavior. That is the part beginner tutorials skip, and it is the part that matters when a dashboard query starts scanning too much data or a report feels slower after you made it cleaner. In practice, the optimizer may inline the CTE, materialize it, or reuse it in a way that depends on the engine.

What changes by engine

PostgreSQL changed the discussion over time. Older versions treated many CTEs like optimization fences, which gave the planner less freedom to reshape the query. Since PostgreSQL 12, non-recursive CTEs can often be inlined, so the engine can optimize across the boundary when that helps.

SQL Server has its own trade-off. Microsoft defines the CTE as a temporary named result set for a single statement, but the optimizer can still choose different physical strategies, including spooling or re-scanning depending on the plan shape. MySQL 8.0 documents CTE support in the WITH clause, and modern behavior often treats non-recursive CTEs as inlineable logical steps rather than permanent intermediates MySQL WITH clause documentation.

BigQuery and Snowflake are different in practice because both run as distributed analytical engines. The CTE reads like a logical step in the SQL, but you still have to verify whether the engine turns that step into a reused intermediate or folds it into the broader plan. That is why the same-looking query can behave differently across warehouses and operational databases.

Rule of thumb: If you reference the same CTE many times, do not assume it is free. Some engines inline it, some may materialize it, and some may re-scan it in ways that only the plan reveals.

A good companion when you are reviewing plans is the guide to database performance tuning. It is useful because CTEs do not remove the need to inspect joins, scans, and predicates, they just make the query easier to reason about before the optimizer gets involved.

A comparative table illustrating how major SQL engines like PostgreSQL, MySQL, and Snowflake execute common table expressions.

The verification step is straightforward. Run EXPLAIN or EXPLAIN ANALYZE, inspect the plan nodes, and confirm whether the CTE is acting like a logical wrapper or an expensive intermediate. If you want a side-by-side look at how that pattern compares with subqueries, a subquery example guide is a useful reference. That is the only reliable way to answer the performance question on your actual engine.

When to Reach for a CTE Instead of a Subquery or Temp Table

A CTE isn't the universal choice, and good SQL gets better when you stop treating it that way. Use it when the logic benefits from a named intermediate step, especially in BI queries that other people need to read, review, or tweak. Reach for something else when persistence, reuse, or cross-statement access matters more than readability.

Same question, three different shapes

Suppose the business question is simple. Find customers with paid revenue above a threshold.

A nested subquery version works, but it can get cramped fast:

SELECT c.id, c.name
FROM customers c
WHERE c.id IN (
  SELECT customer_id
  FROM orders
  WHERE status = 'paid'
  GROUP BY customer_id
  HAVING SUM(order_total) > 1000
);

A CTE chain makes the intermediate logic more obvious:

WITH paid_revenue AS (
  SELECT customer_id, SUM(order_total) AS revenue
  FROM orders
  WHERE status = 'paid'
  GROUP BY customer_id
)
SELECT c.id, c.name
FROM customers c
JOIN paid_revenue pr ON pr.customer_id = c.id
WHERE pr.revenue > 1000;

A temp table or view is better when the result needs to survive beyond one statement, be shared across queries, or be reused in multiple downstream steps. That's why a temp object often wins in ETL-style flows, while a CTE usually wins in a one-off report or alert query.

Option Best for Scope Persistence
CTE Readable one-statement transformations Single statement None
Subquery Simple filter or scalar check Inside one expression None
Temp Table Reuse across statements or later joins Session or workflow dependent Temporary

Draxlr's SQL subquery example guide is a good companion if you want to compare the shapes side by side. The difference is less about style and more about how much naming and reuse your query really needs.

The decision is usually simple in production. If the logic is short and disposable, a subquery is fine. If the logic needs to be audited step by step, a CTE is cleaner. If multiple downstream statements need the same result set, use a temp table or a view.

CTEs in Real Dashboard and Alert Workflows

CTEs show their real value when the SQL has to power something live. A dashboard query that computes per-tenant active users, joins revenue, and filters the final result for a chart is exactly the kind of workload where stepwise SQL pays off. The same pattern also works for scheduled alerts, because the threshold logic can live in one statement and feed a notification without duplicating the calculation.

A practical dashboard query often looks like this:

WITH active_users AS (
  SELECT tenant_id, COUNT(DISTINCT user_id) AS active_user_count
  FROM events
  WHERE event_type = 'session_start'
  GROUP BY tenant_id
),
tenant_revenue AS (
  SELECT tenant_id, SUM(amount) AS revenue
  FROM invoices
  WHERE status = 'paid'
  GROUP BY tenant_id
)
SELECT au.tenant_id, au.active_user_count, tr.revenue
FROM active_users au
JOIN tenant_revenue tr ON tr.tenant_id = au.tenant_id;

That result can feed a chart, a table, or a drill-down in a BI tool. If the same query also needs an alert, the final statement can filter for tenants that cross a rule and hand that result to a scheduled job. Draxlr connects directly to live PostgreSQL, MySQL, Snowflake, BigQuery, and other engines, so a CTE written in the database can flow into dashboards and alerts without rewriting the logic in another layer.

For teams that want the alert path spelled out, the Slack alert setup guide for SQL data changes shows the kind of downstream workflow this pattern supports.

The main advantage here is auditability. When a stakeholder asks where a dashboard number came from, a CTE chain lets you point to each transformation instead of unraveling one giant statement. That makes the query easier to trust, and it makes the alert easier to defend when the threshold changes.

CTE Rules of Thumb Before You Ship the Query

Keep the CTE names readable, so the intermediate step sounds like business logic instead of a parser test. If the same CTE gets referenced over and over, check whether your engine is materializing or re-scanning it, because a temp table may be the better choice. Test recursive CTEs on a small slice first, then run EXPLAIN or EXPLAIN ANALYZE on production-scale data before you call it done.

A comprehensive infographic listing ten essential rules for using Common Table Expressions effectively in SQL queries.

If the query is mostly joins and aggregation, a no-code SQL Query Builder can keep that part visible while the CTE handles the step that benefits from naming. The primary value of CTEs is the combination of readability, debuggability, and engine-aware execution, not syntax for its own sake.


If you're building dashboards, alerts, or embedded analytics on live SQL data, Draxlr lets you write and run CTE-based queries directly against your database, then reuse the result in charts, reports, and notifications. Visit Draxlr to see how a live-query workflow can keep your SQL readable without forcing you to move data around first.

Published via the Outrank app

Start free today

Ready to create SQL Dashboards
& Alerts?

Launch in minutes with your SQL database and ship analytics your team can trust.

Contact usGet Started

No credit card required

This website uses cookies to ensure you get the best experience.