Skip to content
Educora
Intermediate18 min9 / 22

Subqueries

Write a query inside a query: subqueries in WHERE, SELECT and FROM, IN, EXISTS, correlated subqueries and the WITH clause.

Check yourself
In this lesson you will learn
  • Write subqueries that return a single value or a list
  • Use EXISTS and correlated subqueries
  • Break a complex query into parts with a subquery in FROM and with WITH
  • Avoid the NOT IN and NULL trap

“Which products cost more than the average?” This question has two steps: first find the average price (293.56), then compare with it. You could type the number into the query by hand, but tomorrow, when prices change, the query would be out of date. A better way is to do both steps in one query. For that we use a subquery.

A subquery in WHERE

Definition
Subquery

A SELECT query written in brackets inside another query. Its result is used by the outer query as a value, a list or a table.

SQL
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products)
ORDER BY price DESC;
▸ Expected output
name | price
Laptop | 1450
Smartphone | 899.99
Monitor | 310

The inner query runs first and returns one number; the outer query then uses it like an ordinary number. Such a subquery is called scalar: it must return exactly one row and one column. This is also the portable answer to the earlier question “what is the name of the most expensive product?”:

SQL
SELECT name, price
FROM products
WHERE price = (SELECT MAX(price) FROM products);
▸ Expected output
name | price
Laptop | 1450

Subqueries with IN and NOT IN

If a subquery returns many rows in one column, it can serve as the list for IN. Let's find the students enrolled in physics courses. Subqueries can be nested too: the innermost one returns the ids of physics courses, the middle one the ids of students enrolled in them.

SQL
SELECT first_name, last_name
FROM students
WHERE id IN (
  SELECT student_id
  FROM enrollments
  WHERE course_id IN (SELECT id FROM courses WHERE subject = 'Physics')
)
ORDER BY id;
▸ Expected output
first_name | last_name
Murad | Əliyev
Elvin | Quliyev
Rəşad | Kərimov
Fidan | Cəfərova
SQL
SELECT name, category
FROM products
WHERE id NOT IN (SELECT product_id FROM orders)
ORDER BY id;
▸ Expected output
name | category
Monitor | Electronics
The product that was never ordered — the same result as LEFT JOIN ... IS NULL in the last lesson.
SQL
SELECT COUNT(*) AS found
FROM products
WHERE id NOT IN (1, 2, NULL);
▸ Expected output
found
0
Remove NULL from the list and run it again — the result will be 8.

EXISTS and correlated subqueries

A correlated subquery refers to a column of the outer query, for example o.customer_id = c.id. Logically it is recalculated for every row of the outer query. EXISTS (...) is true when the subquery returns at least one row — what the rows contain doesn't matter, so people usually write SELECT 1 inside.

SQL
SELECT c.name, c.country
FROM customers AS c
WHERE EXISTS (
  SELECT 1
  FROM orders AS o
  JOIN products AS p ON p.id = o.product_id
  WHERE o.customer_id = c.id
    AND p.category = 'Electronics'
)
ORDER BY c.id;
▸ Expected output
name | country
Anar Mustafayev | Azerbaijan
Lalə Əhmədova | Azerbaijan
Emre Yılmaz | Türkiye
Zəhra Hüseynli | Azerbaijan
Customers who have bought electronics at least once.

A correlated subquery can also sit in SELECT — then it calculates one value for each row. Below we see how many courses each 10th grader takes and their best score.

SQL
SELECT s.first_name,
       (SELECT COUNT(*)   FROM enrollments AS e WHERE e.student_id = s.id) AS courses,
       (SELECT MAX(score) FROM enrollments AS e WHERE e.student_id = s.id) AS best
FROM students AS s
WHERE s.grade = 10
ORDER BY s.id;
▸ Expected output
first_name | courses | best
Murad | 2 | 81
Rəşad | 2 | 99
Səbinə | 2 | 88
Worked example

In which enrollments is the score higher than the average of that particular course?

Show solution
Each course has its own average, so the subquery must know the course of the outer row.
We use the same table twice and tell the copies apart with aliases: outer e, inner e2.
Condition: e.score > (SELECT AVG(e2.score) FROM enrollments AS e2 WHERE e2.course_id = e.course_id).
The above-average enrollments of every course remain — 9 rows in total.
SQL
SELECT e.student_id, e.course_id, e.score
FROM enrollments AS e
WHERE e.score > (
  SELECT AVG(e2.score)
  FROM enrollments AS e2
  WHERE e2.course_id = e.course_id
)
ORDER BY e.course_id, e.score DESC;
▸ Expected output
student_id | course_id | score
11 | 1 | 97
1 | 1 | 92
3 | 2 | 95
11 | 3 | 94
2 | 3 | 81
4 | 4 | 72
5 | 5 | 93
6 | 6 | 99
3 | 7 | 90

Subqueries in FROM and WITH

A subquery's result is a table, so it can also go in FROM, where it works like a temporary table. Most DBMSs require such a subquery to have an alias (AS t). Below we first find each course's average, then the average of those averages and the best one.

SQL
SELECT ROUND(AVG(avg_score), 1) AS avg_of_courses,
       ROUND(MAX(avg_score), 1) AS best_course
FROM (
  SELECT course_id, AVG(score) AS avg_score
  FROM enrollments
  GROUP BY course_id
) AS t;
▸ Expected output
avg_of_courses | best_course
82 | 92.7

As subqueries pile up inside each other, a query gets hard to read. The WITH clause (a CTE — common table expression) lets you name a subquery up front and then use it like an ordinary table. WITH is supported by SQLite, PostgreSQL, SQL Server and MySQL from version 8.0.

SQL
WITH course_avg AS (
  SELECT course_id, ROUND(AVG(score), 1) AS avg_score
  FROM enrollments
  GROUP BY course_id
)
SELECT c.title, ca.avg_score
FROM course_avg AS ca
JOIN courses AS c ON c.id = ca.course_id
WHERE ca.avg_score > 85
ORDER BY ca.avg_score DESC;
▸ Expected output
title | avg_score
Python Basics | 92.7
World History | 90.5
Algebra | 87.3
Exercise

Show the first name and age of the students who are older than the average age of all students. Sort by age descending, then by first name. Don't type the average in by hand — calculate it with a subquery.

Exercise · SQL
SELECT first_name, age
FROM students
WHERE age > ( /* average age here */ )
ORDER BY age DESC, first_name;
▸ Expected output
first_name | age
Elvin | 17
Fidan | 17
Tural | 17
Murad | 16
Rəşad | 16
Səbinə | 16
Exercise

Show the names of the products ordered by customers from Türkiye (country = 'Türkiye'). Don't use JOIN — use nested IN subqueries. Sort by name.

Exercise · SQL
SELECT name
FROM products
WHERE id IN (
  -- product ids from the orders of Turkish customers
)
ORDER BY name;
▸ Expected output
name
Chess set
Pen set
Smartphone

Key points

  • A subquery is a SELECT in brackets; its result is used as a value, a list or a table.
  • A scalar subquery returns exactly one value and is compared with operators like = and >.
  • IN (SELECT ...) works with a list; NOT IN returns nothing if the list contains NULL.
  • A correlated subquery refers to the outer row; EXISTS checks whether at least one row exists.
  • Give a subquery in FROM an alias; split complex queries into parts with WITH.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
What is a subquery?