Advanced
Subqueries & CTEs
Break complex queries into manageable parts using subqueries, Common Table Expressions (WITH clauses), recursive CTEs, and temporary tables.
Subqueries
A subquery is a query nested inside another query. They can appear in WHERE, FROM, or SELECT clauses.
SQL - Subquery in WHERE
-- Products priced above average SELECT name, price FROM products WHERE price > ( SELECT AVG(price) FROM products ); -- Users who have placed orders SELECT name FROM users WHERE id IN ( SELECT DISTINCT user_id FROM orders );
SQL - Subquery in FROM (Derived Table)
-- Average of category averages SELECT AVG(avg_price) AS overall_avg FROM ( SELECT category, AVG(price) AS avg_price FROM products GROUP BY category ) category_avgs;
Correlated Subqueries
A correlated subquery references columns from the outer query and executes once per outer row:
SQL
-- Products priced above their category average SELECT p.name, p.category, p.price FROM products p WHERE p.price > ( SELECT AVG(p2.price) FROM products p2 WHERE p2.category = p.category ); -- EXISTS: check if related rows exist SELECT u.name FROM users u WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.total > 500 );
Common Table Expressions (CTEs)
CTEs use the WITH keyword to define named temporary result sets. They make complex queries readable and maintainable:
SQL
WITH monthly_revenue AS ( SELECT DATE_TRUNC('month', order_date) AS month, SUM(total) AS revenue FROM orders GROUP BY DATE_TRUNC('month', order_date) ), revenue_with_growth AS ( SELECT month, revenue, LAG(revenue) OVER (ORDER BY month) AS prev_month, ROUND( (revenue - LAG(revenue) OVER (ORDER BY month)) / LAG(revenue) OVER (ORDER BY month) * 100, 1 ) AS growth_pct FROM monthly_revenue ) SELECT * FROM revenue_with_growth ORDER BY month;
Best practice: Prefer CTEs over nested subqueries. CTEs are easier to read, debug, and maintain. You can also reference a CTE multiple times in the same query.
Recursive CTEs
Recursive CTEs reference themselves, useful for hierarchical data like org charts or category trees:
SQL
WITH RECURSIVE org_chart AS ( -- Base case: top-level managers SELECT id, name, manager_id, 1 AS level FROM employees WHERE manager_id IS NULL UNION ALL -- Recursive case: employees under each manager SELECT e.id, e.name, e.manager_id, oc.level + 1 FROM employees e INNER JOIN org_chart oc ON e.manager_id = oc.id ) SELECT * FROM org_chart ORDER BY level, name;
Temporary Tables
SQL
-- Create a temp table for intermediate results CREATE TEMP TABLE high_value_customers AS SELECT u.id, u.name, SUM(o.total) AS lifetime_value FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.id, u.name HAVING SUM(o.total) > 5000; -- Use it in subsequent queries SELECT * FROM high_value_customers ORDER BY lifetime_value DESC;
Lilly Tech Systems