A subquery is a query nested inside another query. A CTE (common table expression) is the same inner query moved to the top of the statement with WITH and given a name, so a long query reads as a series of steps. The examples work in PostgreSQL, MySQL, SQLite and SQL Server.
How to use CTEs and subqueries
A subquery goes where it’s used: in FROM it acts as a table, and in WHERE or SELECT it supplies a value or a list. A CTE is defined first and then used like a table by the query that follows:
-- Subquery: written where it's used
SELECT columns FROM (SELECT ...) AS name;
-- CTE: named first, then used like a table
WITH name AS (SELECT ...)
SELECT columns FROM name;
SELECT n * 2 AS doubled FROM (SELECT 21 AS n) AS numbers;
-- Output: 42
WITH numbers AS (SELECT 21 AS n)
SELECT n * 2 AS doubled FROM numbers;
-- Output: 42
On a real table the two forms return the same rows. Here’s one filter written both ways:
-- Input: people
-- first_name | last_name | job | favorite_food
-- -----------+-----------+----------+--------------
-- Matthew | Ortiz | Chef | cheese
-- Matt | Novak | Pilot | pizza
-- Mattie | Brooks | Engineer | cheese
-- Priya | Nair | Teacher | cheese
-- Subquery
SELECT *
FROM (SELECT first_name, last_name, job FROM people WHERE favorite_food = 'cheese') AS cheese_lovers
WHERE first_name LIKE 'Matt%';
-- CTE
WITH cheese_lovers AS (
SELECT first_name, last_name, job FROM people WHERE favorite_food = 'cheese'
)
SELECT * FROM cheese_lovers
WHERE first_name LIKE 'Matt%';
-- Output of both:
-- first_name | last_name | job
-- -----------+-----------+---------
-- Matthew | Ortiz | Chef
-- Mattie | Brooks | Engineer
Rules to know
-
A CTE only lasts for one statement. The query right after
WITHcan use it; the next statement can’t. To keep a result for several queries, use a temporary table or a view. -
Give a subquery in
FROMa name, likeAS numbersabove. MySQL and SQL Server reject one without it. - A subquery used as a single value must return one row:
SELECT (SELECT 1 UNION ALL SELECT 2) AS n;
-- ERROR: more than one row returned by a subquery used as an expression
-- (SQLite doesn't complain: it quietly uses the first row, 1)
- Speed is usually the same. Databases generally plan a CTE that’s used once the same way as the equivalent subquery, so choose whichever reads better.
Alternative syntax
A CTE that refers to itself (see below) is written WITH RECURSIVE:
-- In SQL Server, leave out RECURSIVE
WITH RECURSIVE counter AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM counter WHERE n < 3
)
SELECT n FROM counter;
-- Output:
-- n
-- -
-- 1
-- 2
-- 3
In SQL Server, the statement before a WITH must end with a semicolon, which is why T-SQL code often starts a CTE with ;WITH.
When should you use a CTE instead of a subquery?
A subquery is fine for one small step. Switch to a CTE when the query has several steps, needs the same result twice, or has to refer to itself.
Filter against one computed value
When the inner query only produces a value or a list to compare against, such as the average order total, a subquery in WHERE is the shortest way to write it. A CTE would work too, but it adds a name nobody needs.
Break a long query into named steps
When a query has stages (total per customer, then the top few, then the final list), give each stage a name. List several CTEs after one WITH, separated by commas: each can use the ones before it, and the query reads top to bottom instead of inside out.
Use the same result more than once
A statement can refer to a CTE as many times as it likes, so a result you need twice (a list of totals and their average, or each total and the grand total) is written once. With subqueries you’d copy the whole inner query.
Walk a hierarchy
Only a CTE can refer to itself. A recursive CTE starts from some rows and keeps joining back to the table until no new rows turn up, which is how you walk an org chart or a category tree, or generate a series of numbers or dates.
Real examples of CTEs and subqueries
Products priced above their category’s average
The subquery refers to p.category from the outer query, so it’s worked out for each product’s own category (a correlated subquery):
-- Input: products
-- name | category | price
-- --------------+-----------+-------
-- Desk Lamp | Lighting | 24.99
-- Floor Lamp | Lighting | 89.00
-- Office Chair | Furniture | 149.00
-- Standing Desk | Furniture | 399.00
-- Bookshelf | Furniture | 120.00
SELECT name, category, price
FROM products p
WHERE price > (
SELECT AVG(price) FROM products WHERE category = p.category
)
ORDER BY category, name;
-- Output:
-- name | category | price
-- --------------+-----------+-------
-- Standing Desk | Furniture | 399.00
-- Floor Lamp | Lighting | 89.00
Each category’s best-selling product
Three steps, each with a name: total the units per product, rank the products within each category, then keep the top one:
-- Input: sales
-- id | product | category | units
-- ---+--------------+-----------+------
-- 1 | Desk Lamp | Lighting | 3
-- 2 | Floor Lamp | Lighting | 5
-- 3 | Desk Lamp | Lighting | 4
-- 4 | Office Chair | Furniture | 2
-- 5 | Bookshelf | Furniture | 6
WITH product_totals AS (
SELECT category, product, SUM(units) AS units
FROM sales
GROUP BY category, product
),
ranked AS (
SELECT category, product, units,
RANK() OVER (PARTITION BY category ORDER BY units DESC) AS place
FROM product_totals
)
SELECT category, product, units
FROM ranked
WHERE place = 1
ORDER BY category;
-- Output:
-- category | product | units
-- ----------+-----------+------
-- Furniture | Bookshelf | 6
-- Lighting | Desk Lamp | 7
Each region’s share of sales
region_totals is used twice: once for the rows, and once inside a subquery for the grand total:
-- Input: orders
-- id | region | amount
-- ---+--------+--------
-- 1 | North | 1200.00
-- 2 | South | 450.00
-- 3 | North | 300.00
-- 4 | West | 800.00
-- 5 | South | 250.00
WITH region_totals AS (
SELECT region, SUM(amount) AS total
FROM orders
GROUP BY region
)
SELECT
region,
total,
ROUND(100.0 * total / (SELECT SUM(total) FROM region_totals), 1) AS pct_of_sales
FROM region_totals
ORDER BY total DESC;
-- Output:
-- region | total | pct_of_sales
-- -------+---------+-------------
-- North | 1500.00 | 50.0
-- West | 800.00 | 26.7
-- South | 700.00 | 23.3
Written with subqueries, the GROUP BY query would appear twice.
An employee’s chain of managers
Start from one employee and follow manager_id upward until it runs out:
-- Input: employees
-- id | name | manager_id
-- ---+-----------+-----------
-- 1 | Maya Chen | NULL
-- 2 | Sam Ortiz | 1
-- 3 | Jo Kim | 2
-- 4 | Lee Ross | 2
-- 5 | Ana Diaz | 1
-- In SQL Server, leave out RECURSIVE
WITH RECURSIVE chain AS (
SELECT id, name, manager_id, 0 AS level
FROM employees
WHERE name = 'Jo Kim'
UNION ALL
SELECT e.id, e.name, e.manager_id, chain.level + 1
FROM employees e
JOIN chain ON e.id = chain.manager_id
)
SELECT level, name FROM chain ORDER BY level;
-- Output:
-- level | name
-- ------+----------
-- 0 | Jo Kim
-- 1 | Sam Ortiz
-- 2 | Maya Chen
Further reading about CTEs and subqueries
On this blog:
- SQL Temp Table Guide: keep an intermediate result for more than one statement
-
SQL Find Duplicates Guide: CTEs with
ROW_NUMBER()to spot repeated rows - SQL Remove Duplicates Guide: deleting duplicates through a CTE
- SQL GROUP BY guide: the aggregates most CTEs are built from
- SQL JOIN Diagrams Guide: joining a CTE to other tables
Official documentation:
- PostgreSQL: WITH queries and subquery expressions
- MySQL: WITH (common table expressions) and subqueries
- SQLite: the WITH clause
- SQL Server: WITH common_table_expression and subqueries
Beekeeper Studio Is A Free & Open Source Database GUI
Best SQL query & editor tool I have ever used. It provides everything I need to manage my database. - ⭐⭐⭐⭐⭐ Mit
Beekeeper Studio is fast, intuitive, and easy to use. Beekeeper supports loads of databases, and works great on Windows, Mac and Linux.
What Users Say About Beekeeper Studio
"Beekeeper Studio completely replaced my old SQL workflow. It's fast, intuitive, and makes database work enjoyable again."
"I've tried many database GUIs, but Beekeeper strikes the perfect balance between features and simplicity. It just works."