DevLearningTools

LEARN · SQL

Multiple CTEs

A single WITH clause can define more than one named CTE, comma-separated, and a later CTE can reference an earlier one, building a readable pipeline of named steps.

Finance wants every employee earning above their own department's average salary, a two-step calculation, first the department averages, then the comparison. One WITH clause can define both steps, named, in order.

Learning Objectives

  • Define more than one CTE in a single WITH clause, comma-separated.
  • Reference an earlier CTE from a later one in the same WITH clause.
  • Know which direction referencing is actually allowed in.

First CTE

dept_avg

Second CTE

above_avg, references dept_avg

Main Query

Reads the final CTE

Result

Each step named and readable

Employees Earning Above Their Department's Average

dept_avg computes each department's average salary first. above_avg, defined right after it with a comma, not a second WITH, joins employees against dept_avg to keep only those earning more than their own department's average. The main query just reads the final, already-filtered result.

MySQL (8+) / MariaDB (10.2+) / PostgreSQL / SQL Server / Oracle / SQLite (3.8.3+)
WITH dept_avg AS (
  SELECT department_id, AVG(salary) AS avg_salary
  FROM employees
  GROUP BY department_id
),
above_avg AS (
  SELECT e.first_name, e.last_name, e.salary, d.avg_salary
  FROM employees e
  JOIN dept_avg d ON e.department_id = d.department_id
  WHERE e.salary > d.avg_salary
)
SELECT * FROM above_avg;

Try It Yourself

Write a query, using two CTEs, that finds every department where the average salary exceeds 75000, then lists all employees in those departments, on MySQL.

Answer
WITH dept_avg AS (
  SELECT department_id, AVG(salary) AS avg_salary
  FROM employees
  GROUP BY department_id
),
high_paying_depts AS (
  SELECT department_id FROM dept_avg WHERE avg_salary > 75000
)
SELECT e.* FROM employees e
JOIN high_paying_depts h ON e.department_id = h.department_id;

Frequently Asked Questions

Can a CTE reference one defined after it in the same WITH clause?

No, only earlier ones. dept_avg can't reference above_avg in the example above, CTEs are resolved in the order they're written, each one can only see the CTEs that came before it in the same WITH clause.

Is there a limit to how many CTEs can be chained in one WITH clause?

No fixed limit on any of these six databases, though a long chain of many CTEs each depending on the last starts to hurt readability well before it hits any real technical limit, splitting the logic across multiple queries, or a view, is usually clearer past a certain point.

Common Beginner Mistakes

Writing a second WITH keyword instead of a comma

WITH dept_avg AS (...) WITH above_avg AS (...) is invalid syntax on every database in this course, multiple CTEs share one single WITH keyword, separated by commas: WITH dept_avg AS (...), above_avg AS (...).

Trying to reference a later CTE from an earlier one

This fails with a "doesn't exist" or "invalid object name" style error, depending on the database, CTEs can only see what's already been defined above them in the same WITH clause, never what comes after.

Best Practices

  • Name each CTE for what it represents (dept_avg, above_avg), not generically (cte1, cte2), the names are the whole readability benefit.
  • Keep each CTE focused on one clear step, chaining several small, obvious steps reads far better than one giant CTE trying to do everything.
  • Reach for a view instead once a chain of CTEs needs to be reused across more than one separate query.

Interview Questions

Write a query, using chained CTEs, to find employees earning more than their department's average salary.

WITH dept_avg AS (SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id), above_avg AS (SELECT e.* FROM employees e JOIN dept_avg d ON e.department_id = d.department_id WHERE e.salary > d.avg_salary) SELECT * FROM above_avg; — dept_avg computes the per-department average first, above_avg then joins the original table against that average to filter.

Why would WITH a AS (...), b AS (SELECT * FROM a ...) work, but WITH b AS (SELECT * FROM a ...), a AS (...) fail?

CTEs in the same WITH clause are resolved strictly in the order they're written, each one can only reference CTEs defined earlier in that same clause. Writing b before a means a doesn't exist yet at the point b tries to reference it, every database in this course rejects that with some form of "object does not exist" error.

Summary

A single WITH clause can define multiple CTEs, comma-separated, with each one able to reference any CTE defined earlier in that same clause, never a later one. This turns a multi-step calculation into a readable, named pipeline, instead of one deeply nested query.

What's Next?

Recursive CTEs are next: a CTE that references itself, the only way to walk a hierarchy, like Beacon Robotics' manager chain, to an unknown depth.

</>

Ready to Practice SQL?

Write and run real SQL queries online, right in your browser, no install or signup required. Pick a playground based on which database you want to practice on.

SQLite / PostgreSQL

Fast, no server, no ads.

Open DB Fiddle ↗

MySQL / SQL Server

Switch databases with one dropdown.

Open SQL Fiddle ↗

Both open a third-party SQL playground in a new tab. External site terms and privacy policy apply.

Or try 50 SQL practice questions →