SQL subqueries (IN, EXISTS, correlated)
A subquery is a query written inside another one, between parentheses. It lets you answer two-step questions in one go, such as “which products cost more than the average?”: you first have to compute the average, then compare each product with that value.
Syntax
SELECT columns FROM table
WHERE column > (SELECT AVG(column) FROM table);
WHERE column IN (SELECT column FROM other_table)
WHERE EXISTS (SELECT 1 FROM other_table WHERE …)How it works
The three most common forms
- A single value: the subquery returns one value, compared with
=,>,<… - A list: the subquery returns a column, used with
INorNOT IN. - An existence test:
EXISTS (…)is true if the subquery returns at least one row.
Correlated subqueries
A correlated subquery refers to the current row of the main query. It is therefore recomputed for each row. Example: comparing each employee's salary with the average of their own department.
In the FROM clause
A subquery can also act as a temporary table in the FROM (you then give it an alias). For complex queries, a CTE (WITH) is often easier to read.
Diagram
| nom | prix |
|---|---|
| Clavier | 49 |
| Plaid | 25 |
| Gourde | 12 |
| Casque | 34 |
| nom | prix |
|---|---|
| Clavier | 49 |
| Casque | 34 |
Simplified diagram to show the idea. The examples below use the real SQL Quest practice database (a fictional online shop: see the full schema).
Glossary: the sample database uses French names
clients- customers
commandes- orders
lignes_commande- order lines
produits- products
fournisseurs- suppliers
employes- employees
prenom / nom- first name / last name (or name)
ville- city
pays- country
prix- price
prix_unitaire- unit price
quantite- quantity
categorie- category
date_commande- order date
poste- job title
departement- department
salaire- salary
statut- status: livrée = delivered, expédiée = shipped, en cours = in progress, annulée = cancelled
Examples with their results
1. Compare with a computed value
The products more expensive than the average price.
SELECT nom, prix
FROM produits
WHERE prix > (SELECT AVG(prix) FROM produits)
ORDER BY prix DESC; Try it in the sandbox| nom | prix |
|---|---|
| Écran 27" | 342 |
| SSD 1To | 317 |
| Souris sans fil | 103 |
| Webcam HD | 101 |
| Casque gaming | 98 |
| Bougie parfumée | 60.92 |
6 rows in total
2. IN with a subquery
The customers who have at least one cancelled order.
SELECT prenom, nom, ville
FROM clients
WHERE id IN (SELECT client_id FROM commandes WHERE statut = 'annulée')
LIMIT 8; Try it in the sandbox| prenom | nom | ville |
|---|---|---|
| Samir | Dubois | Lille |
| Samir | Martin | Lille |
| Zoé | Laurent | Paris |
| Adam | Dubois | Casablanca |
| Alice | Faure | Lyon |
| Léa | Blanc | Toulouse |
| Jade | Garcia | Paris |
| Camille | Laurent | Bordeaux |
8 rows in total
3. Correlated subquery
The employees who earn more than the average of their department.
SELECT e.prenom, e.nom, e.departement, e.salaire
FROM employes e
WHERE e.salaire > (SELECT AVG(x.salaire) FROM employes x WHERE x.departement = e.departement); Try it in the sandbox| prenom | nom | departement | salaire |
|---|---|---|---|
| Jade | Lefebvre | Ventes | 6,200 |
| Nathan | Roux | IT | 6,800 |
| Samir | Simon | RH | 4,800 |
| Alice | Simon | Ventes | 4,800 |
| Lucas | Moreau | Ventes | 4,600 |
| Inès | Mercier | Ventes | 5,500 |
| Chloé | Benali | RH | 4,900 |
| Louis | Chevalier | RH | 5,300 |
| Louis | Garcia | Ventes | 5,500 |
| Lina | Simon | Ventes | 5,200 |
12 rows in total · 10 shown
4. NOT EXISTS
The customers who never ordered, written with NOT EXISTS.
SELECT c.prenom, c.nom
FROM clients c
WHERE NOT EXISTS (SELECT 1 FROM commandes o WHERE o.client_id = c.id); Try it in the sandbox| prenom | nom |
|---|---|
| Manon | Roux |
| Alice | Bernard |
| Emma | Moreau |
| Nathan | Lefebvre |
| Sarah | Simon |
| Karim | Lefebvre |
| Théo | Chevalier |
| Chloé | Lambert |
| Lucas | Benali |
| Inès | Lambert |
10 rows in total
Common mistakes
Using = with a subquery that returns several rows
= expects a single value. SQLite silently takes the first one, other databases return an error. Use IN.
SELECT * FROM clients WHERE id = (SELECT client_id FROM commandes);SELECT * FROM clients WHERE id IN (SELECT client_id FROM commandes);NOT IN with NULLs
If the subquery returns a single NULL, NOT IN no longer returns any rows. NOT EXISTS does not have this problem.
SELECT * FROM employes WHERE id NOT IN (SELECT manager_id FROM employes);SELECT * FROM employes e WHERE NOT EXISTS (SELECT 1 FROM employes x WHERE x.manager_id = e.id);Forgetting the alias of a subquery in FROM
A subquery used as a table must be given a name so it can be used afterwards.
SELECT AVG(nb) FROM (SELECT COUNT(*) AS nb FROM commandes GROUP BY client_id);SELECT AVG(nb) FROM (SELECT COUNT(*) AS nb FROM commandes GROUP BY client_id) AS t;Exercises
Try writing the query yourself before opening the solution.
Challenge 1. Show the most expensive product(s) of each category (correlated subquery).
Show a solution
SELECT categorie, nom, prix FROM produits p WHERE prix = (SELECT MAX(prix) FROM produits WHERE categorie = p.categorie);| categorie | nom | prix |
|---|---|---|
| Informatique | Écran 27" | 342 |
| Maison | Bougie parfumée | 60.92 |
| Sport | Raquette | 46.18 |
| Livres | Manga tome 1 | 28.92 |
| Alimentation | Huile d'olive | 40.06 |
5 rows in total
Try it in the sandboxChallenge 2. Show the employees who do not manage anyone.
Show a solution
SELECT prenom, nom FROM employes e WHERE NOT EXISTS (SELECT 1 FROM employes x WHERE x.manager_id = e.id);| prenom | nom |
|---|---|
| Alice | Simon |
| Nathan | Martin |
| Lucas | Moreau |
| Emma | Lambert |
| Alice | Lefebvre |
| Inès | Mercier |
| Hugo | Bonnet |
| Chloé | Benali |
21 rows in total · 8 shown
Try it in the sandboxThe interactive exercises are checked automatically, with a tutor who explains your mistakes. A short lesson stays next to the editor.
Frequently asked questions
Subquery or join: which one should I choose?
Both can often give the same result. A subquery is easier to read for “filter based on another table”; a join is required to show columns from both tables.
What is the difference between IN and EXISTS?
IN compares a value with a list, EXISTS simply tests whether a row exists. NOT EXISTS is safer than NOT IN when the subquery can contain NULLs.
Is a correlated subquery slow?
Conceptually it runs once per row, which can be costly on large tables. Modern databases often optimize it, but a join or a window function is sometimes faster.