Subqueries, Derived Tables, and Set Operations
Subqueries express multi-step questions; set operations combine and compare result sets (data reconciliation, migration testing).
Concept
Subquery: a SELECT inside another statement — scalar, row, column (IN/ANY/ALL), table (derived table), correlated, or EXISTS.
Derived table: a subquery in FROM (must have an alias in MySQL).
LATERAL derived table (8.0.14+): a derived table that can reference columns of preceding tables.
Set operations: UNION, UNION ALL, INTERSECT, EXCEPT (INTERSECT/EXCEPT since 8.0.31).
Why It Matters
Subqueries express multi-step questions; set operations combine and compare result sets (data reconciliation, migration testing).
Syntax
WHERE col = (SELECT ...) -- scalar
WHERE col IN (SELECT ...) -- column
WHERE (a, b) IN (SELECT x, y ...) -- row
WHERE EXISTS (SELECT 1 FROM ... WHERE correlated) -- existence
FROM (SELECT ...) AS alias -- derived table
FROM t, LATERAL (SELECT ... WHERE u.k = t.k LIMIT 1) AS x
SELECT ... UNION [ALL | DISTINCT] SELECT ...
SELECT ... INTERSECT SELECT ...
SELECT ... EXCEPT SELECT ...Example
13.1 Scalar and column subqueries 13.2 NOT IN vs NOT EXISTS MySQL 8 executes IN/EXISTS subqueries efficiently with semi-join strategies and NOT EXISTS with anti-join (8.0.17+).
13.3 Correlated subquery 13.4 Row subquery 13.5 ANY / ALL 13.6 Derived tables and LATERAL
Limitation: MySQL does not allow LIMIT inside IN (subquery):
13.7 UNION and UNION ALL 13.8 INTERSECT and EXCEPT (8.0.31+) On older versions:
SELECT first_name, salary FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees) -- 117500
ORDER BY salary DESC; -- Asha, Vikram, Rahul
SELECT name FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders); -- Ananya, Ben, Chen, Diya-- Employees who manage nobody
SELECT first_name FROM employees
WHERE emp_id NOT IN (SELECT manager_id FROM employees); -- 0 rows! (a manager_id is NULL)
SELECT e.first_name FROM employees e
WHERE NOT EXISTS (SELECT 1 FROM employees r WHERE r.manager_id = e.emp_id);
-- Neha, Priya, Kiran, MeeraSELECT e.first_name, e.dept_id, e.salary
FROM employees e
WHERE e.salary > (SELECT AVG(e2.salary) FROM employees e2 WHERE e2.dept_id = e.dept_id)
ORDER BY e.dept_id, e.salary DESC;
-- Asha (1), Vikram (1), Rahul (2)SELECT first_name, dept_id, salary
FROM employees
WHERE (dept_id, salary) IN (SELECT dept_id, MAX(salary) FROM employees GROUP BY dept_id)
ORDER BY dept_id;
-- Asha (1, 250000), Rahul (2, 120000), Arjun (3, 90000)SELECT first_name FROM employees
WHERE salary > ALL (SELECT salary FROM employees WHERE dept_id = 2); -- Asha, VikramSELECT ROUND(AVG(t.spent), 2) AS avg_customer_value
FROM (SELECT customer_id, SUM(total_amount) AS spent
FROM orders WHERE status <> 'CANCELLED'
GROUP BY customer_id) AS t; -- 66833.33
-- Latest order per customer with LATERAL (8.0.14+)
SELECT c.name, lo.order_id, lo.order_date
FROM customers c
JOIN LATERAL (
SELECT o.order_id, o.order_date
FROM orders o
WHERE o.customer_id = c.customer_id
ORDER BY o.order_date DESC
LIMIT 1
) AS lo ON TRUE
ORDER BY c.customer_id;+--------------+----------+------------+
| name | order_id | order_date |
+--------------+----------+------------+
| Ananya Gupta | 1003 | 2026-02-02 |
| Ben Carter | 1006 | 2026-03-10 |
| Chen Wei | 1004 | 2026-02-14 |
| Diya Patel | 1005 | 2026-03-01 |
+--------------+----------+------------+SELECT * FROM products WHERE product_id IN (SELECT product_id FROM order_items LIMIT 3);
-- ERROR 1235: This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'
-- Workaround: wrap in a derived table: IN (SELECT product_id FROM (SELECT ... LIMIT 3) x)SELECT city AS place FROM customers
UNION
SELECT location FROM departments
ORDER BY place;
-- Bangalore, Delhi, London, Milan, Mumbai, Pune, Singapore (7 rows; UNION ALL gives 9)SELECT city FROM customers INTERSECT SELECT location FROM departments; -- Mumbai, Pune
SELECT city FROM customers EXCEPT SELECT location FROM departments; -- London, Milan, SingaporeExpected Result
Products never ordered:
SELECT product_name FROM products p
WHERE NOT EXISTS (SELECT 1 FROM order_items oi WHERE oi.product_id = p.product_id);+---------------+
| product_name |
+---------------+
| Standing Desk |
+---------------+Common Mistakes
NOT IN with nullable subquery columns.
Missing derived-table alias (ERROR 1248).
LIMIT inside IN subqueries (ERROR 1235).
UNION where UNION ALL was correct.
INTERSECT/EXCEPT on MySQL < 8.0.31.
Best Practices
Prefer NOT EXISTS for anti-joins.
Use CTEs (Module 14) instead of deep nesting.
Use LATERAL for "top-N per row" lookups with good indexes.
Interview Relevance
High: second-highest salary, NOT IN vs NOT EXISTS, correlated subqueries, UNION vs UNION ALL, INTERSECT support in MySQL.
Practice Questions
Find the second-highest salary with a subquery.
Find customers who ordered both a Laptop and a Monitor.
Find departments whose average salary is above the company average.
Compare customers with a copy table using EXCEPT in both directions.