Skip to content

SQL Subqueries: A Complete Guide with Examples

A subquery is a query nested inside another query. They’re powerful for multi-step data retrieval.

Subquery in WHERE

SELECT name, price
FROM products
WHERE category_id IN (
  SELECT id FROM categories WHERE active = true
);

The inner query runs first, returning active category IDs. The outer query uses them.

Subquery with Comparison

-- Products more expensive than the average
SELECT name, price
FROM products
WHERE price > (
  SELECT AVG(price) FROM products
);

-- The employee with the highest salary
SELECT name, salary
FROM employees
WHERE salary = (
  SELECT MAX(salary) FROM employees
);

Subquery in SELECT

SELECT
  name,
  price,
  (SELECT AVG(price) FROM products) AS avg_price,
  price - (SELECT AVG(price) FROM products) AS price_difference
FROM products;

Useful for adding computed columns that reference other tables or aggregates.

Subquery in FROM

SELECT department, avg_salary
FROM (
  SELECT
    department_id,
    AVG(salary) AS avg_salary
  FROM employees
  GROUP BY department_id
) AS dept_stats
WHERE avg_salary > 50000;

The subquery creates a temporary table that the outer query treats like a regular table.

Correlated Subqueries

A correlated subquery references columns from the outer query. It runs once for each row in the outer query.

-- Employees who earn more than the average in their department
SELECT name, salary, department_id
FROM employees e
WHERE salary > (
  SELECT AVG(salary)
  FROM employees
  WHERE department_id = e.department_id
);

For each employee, the inner query calculates the average salary of that employee’s department.

EXISTS and NOT EXISTS

-- Customers who have placed at least one order
SELECT name, email
FROM customers c
WHERE EXISTS (
  SELECT 1 FROM orders o WHERE o.customer_id = c.id
);

-- Products that have never been ordered
SELECT name
FROM products p
WHERE NOT EXISTS (
  SELECT 1 FROM order_items oi WHERE oi.product_id = p.id
);

EXISTS is often faster than IN for large result sets because it stops as soon as it finds a match.

Subquery with ANY and ALL

-- Products more expensive than ANY product in category 5
SELECT name, price
FROM products
WHERE price > ANY (
  SELECT price FROM products WHERE category_id = 5
);

-- Products more expensive than ALL products in category 5
SELECT name, price
FROM products
WHERE price > ALL (
  SELECT price FROM products WHERE category_id = 5
);

Row Subqueries

-- Find users whose city and state match an office
SELECT name
FROM users
WHERE (city, state) = (
  SELECT city, state FROM offices WHERE is_headquarters = true
);

Nested Subqueries

SELECT name
FROM employees
WHERE department_id IN (
  SELECT id FROM departments
  WHERE location_id IN (
    SELECT id FROM locations WHERE country = 'US'
  )
);

Three levels deep. Each subquery returns results used by its parent.

Subquery vs JOIN

Often the same result can be written as a JOIN or a subquery:

-- Subquery version
SELECT name FROM customers WHERE id IN (SELECT customer_id FROM orders);

-- JOIN version (same result)
SELECT DISTINCT c.name
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id;

When to use which:

SubqueryJOIN
Simple, readable (few columns needed)Better performance with large datasets
EXISTS/NOT EXISTS patternsNeed columns from both tables
Aggregation in WHEREComplex multi-table queries
One-time computationFrequently reused

Performance Tips

  1. Use EXISTS instead of IN when checking for existence in large sets
  2. Use JOIN instead of subquery when you need columns from the subquery table
  3. Add indexes on columns used in subquery WHERE clauses
  4. Limit nested levels — more than 3 levels deep is hard to read and maintain
  5. Use CTEs (WITH clause) for complex queries — they’re more readable and sometimes faster

CTE Alternative

WITH dept_stats AS (
  SELECT department_id, AVG(salary) AS avg_salary
  FROM employees
  GROUP BY department_id
)
SELECT e.name, e.salary, d.avg_salary
FROM employees e
JOIN dept_stats d ON e.department_id = d.department_id
WHERE e.salary > d.avg_salary;

CTEs make complex subqueries easier to read and debug.


Related: Master SQL JOINs and check our SQL cheat sheet.