SQL Quest
Sign in Français 0 XP Lvl 1
Advanced

SQL subqueries (IN, EXISTS, correlated)

4 min read · 4 runnable examples · updated October 9, 2026

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 IN or NOT 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

produits
nomprix
Clavier49
Plaid25
Gourde12
Casque34
result (average = 30)
nomprix
Clavier49
Casque34
WHERE prix > (SELECT AVG(prix) …): the subquery first computes the average (30), then each product is compared with it.

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
Result
nomprix
Écran 27"342
SSD 1To317
Souris sans fil103
Webcam HD101
Casque gaming98
Bougie parfumée60.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
Result
prenomnomville
SamirDuboisLille
SamirMartinLille
ZoéLaurentParis
AdamDuboisCasablanca
AliceFaureLyon
LéaBlancToulouse
JadeGarciaParis
CamilleLaurentBordeaux

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
Result
prenomnomdepartementsalaire
JadeLefebvreVentes6,200
NathanRouxIT6,800
SamirSimonRH4,800
AliceSimonVentes4,800
LucasMoreauVentes4,600
InèsMercierVentes5,500
ChloéBenaliRH4,900
LouisChevalierRH5,300
LouisGarciaVentes5,500
LinaSimonVentes5,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
Result
prenomnom
ManonRoux
AliceBernard
EmmaMoreau
NathanLefebvre
SarahSimon
KarimLefebvre
ThéoChevalier
ChloéLambert
LucasBenali
InèsLambert

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.

Avoid
SELECT * FROM clients WHERE id = (SELECT client_id FROM commandes);
Correct
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.

Avoid
SELECT * FROM employes WHERE id NOT IN (SELECT manager_id FROM employes);
Correct
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.

Avoid
SELECT AVG(nb) FROM (SELECT COUNT(*) AS nb FROM commandes GROUP BY client_id);
Correct
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);
categorienomprix
InformatiqueÉcran 27"342
MaisonBougie parfumée60.92
SportRaquette46.18
LivresManga tome 128.92
AlimentationHuile d'olive40.06

5 rows in total

Try it in the sandbox

Challenge 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);
prenomnom
AliceSimon
NathanMartin
LucasMoreau
EmmaLambert
AliceLefebvre
InèsMercier
HugoBonnet
ChloéBenali

21 rows in total · 8 shown

Try it in the sandbox
Want more practice?

The interactive exercises are checked automatically, with a tutor who explains your mistakes. A short lesson stays next to the editor.

Do the interactive exercises

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.

Read next