Working with Subqueries

1. Creating Scalar Subqueries

PropertyDetail
ReturnsSingle row, single column
UseSELECT list, WHERE, SET clauses
ErrorMultiple rows → "more than one row returned"

Example: Scalar Subquery

SELECT name, salary,
  (SELECT AVG(salary) FROM employees) AS company_avg
FROM employees;

2. Using Subqueries in WHERE Clause

FormExample
INWHERE id IN (SELECT customer_id FROM banned)
NOT INBeware NULLs → use NOT EXISTS
ANY / SOMEWHERE price > ANY(SELECT min_price FROM ...)
ALLWHERE price > ALL(SELECT price FROM other)

3. Using Subqueries in SELECT Clause

UseDetail
Per-row derived columnOne scalar subquery per row
PerformanceOften slower than join — consider LATERAL or window

4. Using Subqueries in FROM Clause

Example: Derived Table

SELECT region, AVG(monthly_total) AS avg_month
FROM (
  SELECT region, date_trunc('month', placed_at) AS mth, SUM(total) AS monthly_total
  FROM orders GROUP BY region, date_trunc('month', placed_at)
) m
GROUP BY region;
AspectDetail
Alias requiredMust give the subquery a name
Multi-step aggregationCommon pattern
CTE alternativeOften more readable

5. Using Correlated Subqueries

PropertyDetail
DefinitionInner query references outer row
ExecutionRe-evaluated per outer row (logically)
Often rewritableAs JOIN + GROUP BY or window function

Example: Correlated Subquery

SELECT e.id, e.name
FROM employees e
WHERE e.salary > (SELECT AVG(salary) FROM employees WHERE dept_id = e.dept_id);

6. Testing Existence

OperatorUse
EXISTSTRUE if subquery returns ≥1 row
NOT EXISTSTRUE if subquery returns 0 rows
Best practicePrefer EXISTS over IN for large inner sets
NULL-safeNOT EXISTS handles NULLs correctly (unlike NOT IN)

7. Comparing with Subqueries

OperatorReturns TRUE When
x = ANY(subq)Equal to any returned value (= IN)
x > ANY(subq)Greater than at least one
x > ALL(subq)Greater than every value
x <> ALL(subq)Not equal to any (= NOT IN)

8. Optimizing Subquery Performance

TechniqueDetail
Rewrite to JOINOften faster, planner has more options
Use EXISTS over INShort-circuits; NULL-safe
Materialize CTEPostgres 12+ inlines by default; force with MATERIALIZED
LATERAL JOINPer-row subqueries with planner help
Window functionsAvoid correlated aggregates

9. Converting Subqueries to Joins

Example: IN → JOIN

-- Subquery form
SELECT * FROM orders
WHERE customer_id IN (SELECT id FROM customers WHERE region='EU');

-- JOIN form (often faster)
SELECT o.* FROM orders o
JOIN customers c ON c.id = o.customer_id AND c.region='EU';
SubqueryEquivalent Join
ININNER JOIN (deduplicate as needed)
NOT IN / NOT EXISTSLEFT JOIN ... WHERE right IS NULL (anti-join)
EXISTSSEMI JOIN (planner often picks this)

10. Using Common Table Expressions

FormDetail
WITH name AS (...)Named subquery, scope: outer query
Multiple CTEsComma-separated; later refers to earlier
WITH RECURSIVESelf-referencing for trees/graphs
MATERIALIZED / NOT MATERIALIZED (PG)Control inlining

Example: Recursive CTE (Hierarchy)

WITH RECURSIVE org AS (
  SELECT id, name, manager_id, 1 AS lvl
  FROM employees WHERE manager_id IS NULL
  UNION ALL
  SELECT e.id, e.name, e.manager_id, o.lvl + 1
  FROM employees e JOIN org o ON e.manager_id = o.id
)
SELECT * FROM org ORDER BY lvl, name;