SQL CTE (WITH Clause)

A SQL CTE (common table expression) is a named temporary result defined with the WITH clause at the start of a query. It does the same job as a subquery in FROM but reads top to bottom, which makes long queries much easier to follow.

A Simple CTE

First compute spending per customer, then join it to names.

WITH spend AS (
  SELECT o.CustomerID, SUM(o.Quantity * p.Price) AS total
  FROM Orders o JOIN Products p ON p.ProductID = o.ProductID
  GROUP BY o.CustomerID
)
SELECT c.CustomerName, s.total
FROM spend s JOIN Customers c ON c.CustomerID = s.CustomerID
ORDER BY s.total DESC;
CustomerName   | total
---------------+------
Blue Sky Foods | 270
Harbor Goods   | 264
Golden Grains  | 240
Alpha Traders  | 237.5
Evergreen Co   | 170
Cedar Market   | 150.5
Indigo Retail  | 90
(7 rows)

Multiple CTEs

Separate several CTEs with commas; later ones can use earlier ones. Here we filter by a window function, which needs this two-step approach.

WITH ranked AS (
  SELECT Category, ProductName, Price,
         ROW_NUMBER() OVER (PARTITION BY Category ORDER BY Price DESC) AS rn
  FROM Products
),
top_per_cat AS (
  SELECT * FROM ranked WHERE rn = 1
)
SELECT Category, ProductName, Price FROM top_per_cat ORDER BY Price DESC;
Category   | ProductName  | Price
-----------+--------------+------
Beverages  | Coffee Beans | 22
Oils       | Olive Oil    | 18.75
Snacks     | Almonds      | 15
Grains     | Basmati Rice | 12.5
Sweeteners | Honey        | 11.5
(5 rows)

Recursive CTE

WITH RECURSIVE lets a CTE refer to itself, useful for hierarchies and number or date series. This generates the first 5 days of June. SQL Server writes just WITH.

WITH RECURSIVE days(d) AS (
  SELECT '2026-06-01'
  UNION ALL
  SELECT date(d, '+1 day') FROM days WHERE d < '2026-06-05'
)
SELECT d FROM days;
d
----------
2026-06-01
2026-06-02
2026-06-03
2026-06-04
2026-06-05
(5 rows)

CTE vs Subquery

  • CTEs are easier to read and can be referenced several times
  • Performance is usually the same; the optimizer inlines them
  • Only CTEs can be recursive

Frequently asked questions

Is a CTE faster than a subquery?

Usually they perform the same. Choose CTEs for readability. PostgreSQL 12+ and others inline simple CTEs automatically.

Can I use a CTE with UPDATE or DELETE?

Yes, in most databases a WITH clause can precede INSERT, UPDATE or DELETE statements.

Does a CTE create a temporary table?

No, it exists only while the single statement runs. For reuse across statements, create a temporary table or a view.