Correlated Subqueries & EXISTS
Subqueries that depend on the outer row, the EXISTS / NOT EXISTS test, and when to use a JOIN instead.
"Who scored more than their own team's average?" The average is different for each team, so it cannot be calculated once. The inner query must be re-run for each player, using that player's team. That is a correlated subquery.
Definition
A non-correlated subquery is independent of the outer query: it can run on its own, and it runs once before the outer query uses its result. A correlated subquery refers to a column of the outer query's current row, so it cannot run on its own and is (logically) evaluated once per outer row.
The link is an outer column used inside the subquery, usually through an alias: WHERE q.team = p.team — here p belongs to the outer query.
| Non-correlated | Correlated | |
|---|---|---|
| Depends on outer query? | No — independent | Yes — uses the outer row's values |
| Execution | Runs ONCE, before the main query | Runs ONCE PER outer row (logically) |
| Can run on its own? | Yes | No — the outer alias is unknown |
| Performance | Usually faster | Can be slower on big tables |
| Example | runs > (SELECT AVG(runs) FROM players) | runs > (SELECT AVG(runs) … WHERE q.team = p.team) |
Note: "Once per row" describes the meaning. The SQL Server optimizer often rewrites correlated subqueries into joins internally, so they are not always slow — but on large tables, check the execution plan.
Syntax — correlated subquery
sql
SELECT o.column_list
FROM table_name AS o -- outer alias
WHERE o.column > (
SELECT aggregate(i.column)
FROM table_name AS i -- inner alias
WHERE i.link_column = o.link_column -- the correlation
);Example — above their own team's average
sql
SELECT id, name, team, runs
FROM players AS p
WHERE runs > (
SELECT AVG(runs)
FROM players AS q
WHERE q.team = p.team
);| id | name | team | runs |
|---|---|---|---|
| 2 | Rohit Sharma | MI | 500 |
How it works:
- Take one player row
p(e.g. Rohit, team MI). - Run the inner query for his team → MI average = (500 + 20) / 2 = 260.
- Keep the row if 500 > 260 → yes.
Every other team has only one player, so each of them equals (not beats) their team average. Only Rohit survives.
Definition — EXISTS
EXISTS is a logical operator that returns TRUE if the subquery returns at least one row, and FALSE if it returns no rows. NOT EXISTS is the opposite. The values the subquery selects are ignored — only "any row or no row" matters.
That is why people write SELECT 1 inside EXISTS — the 1 is irrelevant; SELECT * works the same.
Syntax — EXISTS
sql
SELECT columns
FROM table_a AS a
WHERE EXISTS (
SELECT 1
FROM table_b AS b
WHERE b.link_column = a.link_column
);Example — players with an Orange Cap
sql
SELECT id, name, runs
FROM players AS p
WHERE EXISTS (
SELECT 1
FROM orange_cap AS o
WHERE o.player_id = p.id
);| id | name | runs |
|---|---|---|
| 1 | Virat Kohli | 750 |
| 3 | Jos Buttler | 850 |
Step by step: take one player row → run the inner query for it → any row found? keep the player. The database can stop searching at the first match, which makes EXISTS efficient.
Example — NOT EXISTS: teams with no players
sql
SELECT t.team, t.team_name
FROM teams AS t
WHERE NOT EXISTS (
SELECT 1
FROM players AS p
WHERE p.team = t.team
);| team | team_name |
|---|---|
| KKR | Kolkata Knight Riders |
| GT | Gujarat Titans |
NOT EXISTS is NULL-safe, unlike NOT IN, which is why it is the recommended way to write "rows with no match".
JOIN vs subquery — same answer
Often both work. Pick the clearer one.
sql
-- Subquery: great for filtering / existence checks
SELECT name, runs
FROM players
WHERE id IN (SELECT player_id FROM orange_cap);
-- JOIN: needed when you want columns from BOTH tables
SELECT p.name, p.runs, o.season
FROM players AS p
INNER JOIN orange_cap AS o
ON p.id = o.player_id;| name | runs | season |
|---|---|---|
| Virat Kohli | 750 | 2024 |
| Jos Buttler | 850 | 2022 |
| Use a subquery when… | Use a JOIN when… |
|---|---|
| you only filter on the other table ("has / has no match") | you need to display columns from both tables |
| you compare with an aggregate (average, max) | you combine many tables in one result |
| duplicates must not appear (IN / EXISTS never duplicate outer rows) | the relationship is one-to-one or you handle duplicates |
Tip: A JOIN can duplicate outer rows if the other table has several matches (e.g. a player who won the Orange Cap twice).
IN/EXISTSnever duplicate.
Common mistakes
- ❌ Forgetting the correlation:
WHERE EXISTS (SELECT 1 FROM orange_cap)→ TRUE for every player as long as the table has any row. ✅ addWHERE o.player_id = p.id. - ❌ Using the same table twice without aliases → SQL cannot tell which
teamyou mean. ✅ alias both (pandq). - ❌ Trying to run a correlated subquery on its own to debug it → error "could not be bound". ✅ replace the outer column with a literal value to test it.
Interview tip
Q: EXISTS vs IN — which is better?
Both find matching rows. EXISTS stops at the first match and is NULL-safe in its NOT EXISTS form, so it is the safer choice for "has no match" checks. For small lists, modern optimizers usually produce the same plan for IN and EXISTS.
Key points to remember
- A non-correlated subquery is independent and runs once; a correlated subquery uses the outer row's values and runs once per outer row.
- A correlated subquery cannot run on its own — it references an outer alias like p.team.
- EXISTS returns TRUE if the subquery returns at least one row; the selected value (SELECT 1) is irrelevant.
- NOT EXISTS is NULL-safe and is the recommended way to find rows with no match.
- EXISTS can stop at the first matching row, which makes it efficient.
- Use a subquery for filtering / existence checks and a JOIN when you need columns from both tables.
- A JOIN can duplicate outer rows on multiple matches; IN and EXISTS never do.
Try it yourself
Above their own team's average (correlated)
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Correlated subquery in SELECT: each player's team total
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
EXISTS and NOT EXISTS
This example runs a small setup step first — click to see it
CREATE TABLE orange_cap (player_id INT, season INT); INSERT INTO orange_cap VALUES (1, 2024), (3, 2022);
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Practice: top scorer of each team
Runs on a fresh IPL practice database in your browser · edit the query and press Ctrl + Enter
Revision Flashcards
Tap a card to flip it and see the answer.
