Subqueries in SELECT, FROM and WHERE: Join vs Subquery vs EXISTS
The same question answered four ways, with a straight comparison of readability, NULL safety and cost - and a rule for choosing between them.
- SQL Basics
- DDL
- DML
- SELECT
- WHERE
- Functions
- NULL and Logic
- Aggregate Functions
- GROUP BY
- JOIN
- Subqueries
- Set Operations
- CTEs
- Constraints
- Keys
- Relationships
- Database Design
- Normalisation
- Views
- Window Functions
- Advanced SQL
- Procedures and Functions
- Triggers
- Temporary Tables
- Transactions
- Isolation and Locking
- Indexes
- Query Performance
- SQL Security
- SQL Dialects
- Practical SQL
Concept
Most questions can be written as a join, a subquery or an EXISTS check. They are not interchangeable in every case, and the differences that matter are row multiplication, NULL safety and cost.
Subqueries in each position
-- In WHERE: filter the outer rows
SELECT name FROM customers
WHERE id IN (SELECT customer_id FROM orders WHERE status = 'shipped');
-- In SELECT: add a computed column, one value per outer row
SELECT c.name,
(SELECT MAX(o.order_date) FROM orders o WHERE o.customer_id = c.id) AS last_order
FROM customers c;
-- In FROM: treat a query result as a table (a derived table)
SELECT t.country, t.customers, t.avg_orders
FROM (SELECT c.country,
COUNT(DISTINCT c.id) AS customers,
ROUND(COUNT(o.id) * 1.0 / COUNT(DISTINCT c.id), 2) AS avg_orders
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.country) t
WHERE t.customers > 1;A subquery in FROM is a derived table and must have an alias in MySQL, MariaDB and PostgreSQL - omitting it is a syntax error.
The same question, four ways
Which customers have placed at least one shipped order?
-- 1. JOIN with DISTINCT
SELECT DISTINCT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id AND o.status = 'shipped';
-- 2. IN with a subquery
SELECT name FROM customers
WHERE id IN (SELECT customer_id FROM orders WHERE status = 'shipped');
-- 3. EXISTS
SELECT c.name FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o
WHERE o.customer_id = c.id AND o.status = 'shipped');
-- 4. Pre aggregated join
SELECT c.name
FROM customers c
JOIN (SELECT DISTINCT customer_id FROM orders WHERE status = 'shipped') s
ON s.customer_id = c.id;| Duplicates rows | NULL safe | Can return child columns | Typical use | |
|---|---|---|---|---|
JOIN | Yes - needs DISTINCT | n/a | Yes | You need data from both tables |
IN | No | Yes for IN, no for NOT IN | No | Small, fixed lists |
EXISTS | No | Yes | No | Existence checks, anti joins |
| Derived table | Controlled by you | Yes | Yes, aggregated | Aggregating the many side first |
Explanation
The deciding question is simple: do you need columns from the other table?
- Yes, and one row each - use a
JOIN. - Yes, but the other table has many rows per parent - aggregate it in a derived table or CTE, then join.
- No, you only need to know whether a match exists - use
EXISTSorNOT EXISTS.
Performance rarely decides it any more. On modern optimisers IN, EXISTS and a semi join are frequently compiled to the same plan. Where they differ, EXISTS tends to win on large child tables because it stops at the first match, and the pre aggregated join wins when the child aggregate is needed anyway.
Important rules
- A derived table needs an alias in MySQL, MariaDB and PostgreSQL.
- A subquery in
SELECTmust return one row and one column. - A join to the many side multiplies parent rows;
INandEXISTSnever do. NOT INis unsafe against nullable columns;NOT EXISTSandLEFT JOIN ... IS NULLare not.- A CTE (
WITH) is a named derived table and is almost always more readable than nesting.
Common mistakes
- Joining to check existence, then sprinkling
DISTINCTto clean up the duplicates. - Using
NOT INon a nullable column. - Nesting derived tables three deep instead of using CTEs.
- Assuming a subquery is always slower than a join - measure before rewriting.
Best practices
- Choose by intent:
JOINto combine data,EXISTSto test, derived table or CTE to reshape. - Prefer a CTE to a nested derived table for anything more than one level deep.
- Read the execution plan before optimising - see the note on
EXPLAIN. - Keep the anti join pattern consistent across the codebase so reviewers recognise it instantly.
Practice
- Answer which products have been ordered at least once in all four styles above.
- Return each country with its customer count and total revenue, using a derived table.
- For each style, say whether it can also return the order date, and why.