Skip to main content
eLearner.app
Module 7 · Lesson 1 of 425/57 in the course~12 min
Module lessons (1/4)

CTE: multi-step queries with WITH

Real queries tend to grow: three JOINs, a GROUP BY, a subquery, another subquery… and they become unreadable. CTEs ("Common Table Expressions", the WITH clause) let you give a name to an intermediate step and use it as if it were a table in the main query.

Basic syntax

SQL
WITH nome_cte AS (
  SELECT-- query interna
)
SELECT-- query principale, usa nome_cte come fosse una tabella
FROM   nome_cte;

A CTE is valid only for the query that follows it: it does not survive the execution.

Example

Find the 3 customers who have spent the most (sum over delivered orders):

SQL
WITH total_spent AS (
  SELECT o.customer_id,
         SUM(oi.quantity * oi.unit_price) AS total
  FROM   orders      o
  JOIN   order_items oi ON oi.order_id = o.id
  WHERE  o.status = 'delivered'
  GROUP BY o.customer_id
)
SELECT c.email,
       s.total
FROM   total_spent s
JOIN   customers c ON c.id = s.customer_id
ORDER BY s.total DESC
LIMIT  3;

Read in English: "first compute the spend per customer; then join with customers and take the top 3". Without a CTE you'd need an inline subquery in the FROM (legal, but noisier).

Multiple CTEs in cascade

You can define multiple CTEs separated by commas, and each one can refer to the previous ones:

SQL
WITH total_spent AS (
  SELECT customer_id, SUM(quantity * unit_price) AS total
  FROM   order_items oi
  JOIN   orders      o ON oi.order_id = o.id
  WHERE  o.status = 'delivered'
  GROUP BY customer_id
),
average AS (
  SELECT AVG(total) AS total_average
  FROM   total_spent
)
SELECT c.email, s.total
FROM   total_spent s
JOIN   customers c ON c.id = s.customer_id
CROSS JOIN average
WHERE  s.total > average.total_average
ORDER BY s.total DESC;

CROSS JOIN average because average always has a single row: the Cartesian product just appends total_average to every row of the result.

CTE vs subquery: when to use which

CTEs shine when:

  • the same subquery is used two or more times in the main query;
  • you want to make the structure explicit in named steps (self-documenting);
  • they are the prerequisite for recursive CTEs (WITH RECURSIVE, the next lesson in this module).

An inline subquery is more concise for a step used only once.

Try it

Exercise#sql.m7.l1.e1
Attempts: 0Loading…

Use a CTE named 'total_spent' that computes, for each customer, the total spend (SUM(quantity * unit_price)) on delivered orders. Then in the main query show email and total for the top 3 spenders. Order by descending total, limit to 3.

Loading editor…
Show hint

Inside the CTE sum quantity * unit_price. In the main query JOIN customers on id.

Solution available after 3 attempts

Review exercise

Exercise#sql.m7.l1.e2
Attempts: 0Loading…

Same 'total_spent' CTE as the previous exercise. In the main query show email and total for the customers whose spend is above the average spend (across all customers with at least one delivered order). Order by descending total.

Loading editor…
Show hint

Scalar subquery in the WHERE: SELECT AVG(total) FROM total_spent.

Solution available after 3 attempts