SQL

Subqueries

SQL · 8 entries

Scalar Subquery

syntax
SELECT column, (SELECT single_value FROM ...) AS alias
FROM table;
example
SELECT
  product_name,
  price,
  price - (SELECT AVG(price) FROM products) AS diff_from_avg
FROM products;
output
-- product_name | price  | diff_from_avg
-- Laptop       | 999.00 | 849.50
-- Cable         | 9.99   | -139.51

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.

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.

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.

Derived Table (Subquery in FROM)

syntax
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.

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.

Subquery with ANY / ALL

syntax
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.

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.

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.