Note A scalar subquery must return exactly one row and one column. If it returns more than one row, the query fails. If it returns zero rows, the result is NULL.
scalar subquerysingle value subquerysubquery in select
Subquery in WHERE with IN
syntax
WHERE column IN (SELECT column FROM ...)
example
SELECT first_name, email
FROM users
WHERE id IN (
SELECT user_id
FROM orders
WHERE total_amount > 500
);
output
-- Users who have placed at least one order over 500
Note For large datasets, EXISTS often performs better than IN with a subquery because EXISTS short-circuits on the first match. IN materializes the entire subquery result first.
subquery in wherein subqueryfilter with subquery
Correlated Subquery
syntax
SELECT columns
FROM table1 t1
WHERE column op (
SELECT aggregate(col) FROM table2 t2
WHERE t2.ref = t1.id
);
example
SELECT product_name, price, category_id
FROM products p
WHERE price > (
SELECT AVG(price)
FROM products
WHERE category_id = p.category_id
);
output
-- Products priced above their own category's average
Note A correlated subquery references the outer query and executes once per outer row. This can be slow on large tables. Consider rewriting as a JOIN with a derived table for better performance.
SELECT columns
FROM (SELECT ... FROM ...) AS alias;
example
SELECT
top_customers.first_name,
top_customers.total_spent
FROM (
SELECT u.first_name, SUM(o.total_amount) AS total_spent
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.first_name
HAVING SUM(o.total_amount) > 1000
) AS top_customers
ORDER BY top_customers.total_spent DESC;
output
-- first_name | total_spent
-- Alice | 4520.00
-- Bob | 2310.50
Note Derived tables (subqueries in FROM) must have an alias. They are computed once and treated like a temporary table. CTEs (WITH clause) are generally more readable for the same purpose.
subquery in fromderived tableinline viewtemporary table subquery
EXISTS Subquery
syntax
WHERE EXISTS (SELECT 1 FROM table WHERE condition)
example
SELECT c.category_name
FROM categories c
WHERE NOT EXISTS (
SELECT 1 FROM products p
WHERE p.category_id = c.id
);
output
-- Categories that have no products at all
Note NOT EXISTS is the safest way to find missing related rows. Unlike NOT IN, it handles NULLs correctly and will not produce unexpected empty results.
WHERE column > ANY (subquery)
WHERE column > ALL (subquery)
example
SELECT first_name, salary
FROM employees
WHERE salary > ALL (
SELECT salary
FROM employees
WHERE department_id = 3
);
output
-- Employees earning more than everyone in department 3
Note ANY means 'at least one' — the condition must hold for at least one row from the subquery. ALL means 'every' — it must hold for all rows. An empty subquery makes ALL true and ANY false.
any subqueryall subquerycompare to subquery set
LATERAL Join (Subquery)
syntax
SELECT columns
FROM table1 t1
CROSS JOIN LATERAL (
SELECT ... FROM table2 WHERE ref = t1.id LIMIT n
) AS sub;
example
SELECT u.first_name, recent.order_id, recent.total_amount
FROM users u
CROSS JOIN LATERAL (
SELECT order_id, total_amount
FROM orders
WHERE user_id = u.id
ORDER BY order_date DESC
LIMIT 3
) AS recent;
output
-- Each user paired with their 3 most recent orders
Note LATERAL lets a subquery reference columns from preceding tables in the FROM clause. It is the SQL equivalent of a for-each loop. PostgreSQL supports LATERAL; MySQL 8+ supports LATERAL derived tables.
lateral jointop n per groupfor each row subquerylateral subquery
Subquery in INSERT
syntax
INSERT INTO target (columns)
SELECT columns FROM source WHERE condition;
example
INSERT INTO archived_orders (order_id, user_id, total_amount, order_date)
SELECT order_id, user_id, total_amount, order_date
FROM orders
WHERE order_date < '2024-01-01';
output
-- Copies old orders into an archive table
Note The SELECT column count and types must match the INSERT column list. This is an efficient way to bulk-copy data between tables without round-tripping through the application.
insert from selectcopy data between tablesbulk insert subquery