ShareYourGroupShareYourGroupSQL

Lesson 17 of 26 · see the practice database

Subqueries

A subquery is a query written inside another query, in brackets. The inner query runs and hands its result to the outer one. It lets you answer in one go a question that would otherwise take two steps: first find the average price, then find the products above it.

A subquery that returns one value

Lesson 11 showed that WHERE price > AVG(price) is not allowed. A subquery supplies the average as a plain number.

SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products)
ORDER BY price;

Result

nameprice
Earphones499
Basmati Rice 5 kg650
School Bag750
Bedsheet Double899
Power Bank1299
Pressure Cooker 3 L1450

6 rows

The inner query returns one row with one column. Such a subquery can stand anywhere a single value can. You can run the inner part alone to see what it gives, which is the first step in understanding any query that contains one.

Who has the highest salary?

SELECT name, salary
FROM employees
WHERE salary = (SELECT MAX(salary) FROM employees);

Result

namesalary
Anita Kapoor95000

1 row

The second highest salary, a favourite interview question, is the highest among the salaries below the highest.

SELECT MAX(salary) AS second_highest
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);

Result

second_highest
60000

1 row

A subquery that returns a list: IN

When the inner query returns one column with several rows, use IN.

SELECT name, city
FROM customers
WHERE id IN (SELECT customer_id FROM orders WHERE status = 'cancelled');

Result

namecity
Kabir KhanMumbai
Neha JoshiJaipur

2 rows

The inner query makes a list of customer numbers with a cancelled order. The outer query shows the customers whose id is in that list. A customer is shown once even if they cancelled several times, which a join would not do without DISTINCT.

Products that appear in order 7:

SELECT name
FROM products
WHERE id IN (SELECT product_id FROM order_items WHERE order_id = 7);

Result

name
Notebook 200 pages
Ball Pen pack of 10
Geometry Box

3 rows

NOT IN, and its trap

NOT IN finds the rows that are missing from the list. These are the customers who have never ordered.

SELECT name
FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders);

Result

name
Meena Gill
Arjun Mehta
Tara Singh

3 rows

Now the trap promised in the lesson on NULL. If the list contains even one NULL, NOT IN returns nothing at all. Try to find the employees who are not anybody's manager:

SELECT name
FROM employees
WHERE id NOT IN (SELECT manager_id FROM employees);

Result

name

0 rows

No rows, though several employees manage nobody. The list of manager numbers includes the NULL of the person at the top, and "is not equal to unknown" is never true. Remove the NULLs inside the subquery, or use NOT EXISTS, below.

SELECT name
FROM employees
WHERE id NOT IN (SELECT manager_id FROM employees WHERE manager_id IS NOT NULL);

Result

name
Deepa Iyer
Manoj Yadav
Gopal Reddy
Nikhil Jain

4 rows

EXISTS

EXISTS asks only one thing of a subquery: does it return any row at all? It is true or false.

SELECT c.name
FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

Result

name
Asha Verma
Ravi Kumar
Kabir Khan
Sunita Rao
Priya Nair
Imran Ali
Neha Joshi
Vikram Das
Joy Thomas

9 rows

Look at the inner query. It mentions c.id, a column of the outer query. So it cannot run by itself. The database works through the customers one by one, and for each asks, "is there any order with this customer's number?" A subquery that refers to the outer query is called a correlated subquery.

What the inner SELECT lists does not matter to EXISTS, so people write SELECT 1.

NOT EXISTS is the safe way to find what is missing. It has no trouble with NULL.

SELECT c.name
FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

Result

name
Meena Gill
Arjun Mehta
Tara Singh

3 rows

SELECT e.name
FROM employees e
WHERE NOT EXISTS (SELECT 1 FROM employees x WHERE x.manager_id = e.id);

Result

name
Deepa Iyer
Manoj Yadav
Gopal Reddy
Nikhil Jain

4 rows

A subquery in the SELECT list

A subquery that returns one value can be a column. It is worked out for each row of the outer query.

SELECT c.name,
       (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id) AS orders
FROM customers c
ORDER BY orders DESC, c.name;

Result

nameorders
Asha Verma4
Kabir Khan3
Ravi Kumar3
Imran Ali2
Neha Joshi2
Priya Nair2
Sunita Rao2
Joy Thomas1
Vikram Das1
Arjun Mehta0
Meena Gill0
Tara Singh0

12 rows

Customers with no orders get 0, with no left join needed. It is neat for one extra figure. For several figures from the same table, a join with GROUP BY is clearer and usually faster.

A subquery also lets you compare each row with a total.

SELECT name, price,
       ROUND(100.0 * price / (SELECT SUM(price) FROM products), 1) AS percent_of_all
FROM products
ORDER BY price DESC
LIMIT 4;

Result

namepricepercent_of_all
Pressure Cooker 3 L145018.8
Power Bank129916.8
Bedsheet Double89911.7
School Bag7509.7

4 rows

Comparing with the row's own group

A correlated subquery can compare each row with a figure for its own group. These are the products that cost more than the average of their own category.

SELECT p.name, p.category, p.price
FROM products p
WHERE p.price > (SELECT AVG(x.price) FROM products x WHERE x.category = p.category)
ORDER BY p.category, p.price;

Result

namecategoryprice
Power BankElectronics1299
Basmati Rice 5 kgGrocery650
Bedsheet DoubleHome899
Pressure Cooker 3 LHome1450
School BagStationery750

5 rows

The same table is used twice, with two aliases, as in a self join. The highest-paid person in each department works the same way.

SELECT e.name, e.department, e.salary
FROM employees e
WHERE e.salary = (SELECT MAX(x.salary) FROM employees x WHERE x.department = e.department)
ORDER BY e.department;

Result

namedepartmentsalary
Anita KapoorManagement95000
Rohit BansalSales60000
Harpreet KaurSupport36000
Farah SheikhWarehouse42000

4 rows

A subquery in FROM

The result of a query is a table, so it can be used as one. Put the subquery in the FROM and give it an alias. This is how you summarise a summary. First count the orders for each customer, then average those counts.

SELECT ROUND(AVG(n), 2) AS average_orders_per_customer
FROM (
  SELECT customer_id, COUNT(*) AS n
  FROM orders
  GROUP BY customer_id
) AS per_customer;

Result

average_orders_per_customer
2.22

1 row

A subquery in FROM is often called a derived table. Most databases insist on the alias. The lesson on views and WITH shows a tidier way to write the same thing.

Subquery or join?

Many questions can be answered either way.

Modern databases often run the two forms equally fast. Choose the one that is easier to read and to get right.

Rules to remember

Try it yourself

Exercise 1. Show the names and salaries of the employees who earn less than the average salary.

Solve it in the practice box

Show one answer
SELECT name, salary FROM employees WHERE salary < (SELECT AVG(salary) FROM employees);
namesalary
Deepa Iyer48000
Manoj Yadav48000
Farah Sheikh42000
Gopal Reddy30000
Harpreet Kaur36000
Nikhil Jain33000

6 rows

Exercise 2. Using IN with a subquery, show the names of the products that were part of order 20.

Solve it in the practice box

Show one answer
SELECT name FROM products WHERE id IN (SELECT product_id FROM order_items WHERE order_id = 20);
name
Notebook 200 pages
Ball Pen pack of 10
School Bag

3 rows

Exercise 3. Show the name of the product, or products, with the lowest price.

Solve it in the practice box

Show one answer
SELECT name FROM products WHERE price = (SELECT MIN(price) FROM products);
name
Notebook 200 pages

1 row

Examples on this page were run with SQLite 3.50.4 on 12 November 2026. The results shown are the real results.

This is an independent learning site. It is not connected with the makers of any database product. MySQL, PostgreSQL, SQL Server, Oracle and SQLite are named only so that you know where a piece of SQL works. The shop, its customers and its staff in the practice database are made up.