Skillversitas

CTEs and Subqueries

Complex analytical questions rarely fit into a single SELECT. DuckDB, following standard SQL, offers two mechanisms for breaking work into named, readable steps: subqueries, which nest one query inside another, and common table expressions (CTEs), which declare intermediate result sets with the WITH clause.

Subqueries

Three placements matter in practice. A scalar subquery returns a single value used in an expression, an IN/EXISTS subquery filters against a set, and a derived table sits in the FROM clause as a temporary named relation:

-- scalar subquery in the SELECT list
SELECT name, salary,
       (SELECT avg(salary) FROM employees) AS company_avg
FROM employees;

-- EXISTS filter
SELECT c.name FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o
              WHERE o.customer_id = c.id AND o.amount > 100);

-- derived table
SELECT d.dept, d.avg_sal
FROM (SELECT dept, avg(salary) AS avg_sal
      FROM employees GROUP BY dept) d
WHERE d.avg_sal > 60000;

A subquery that references columns of the outer query, like the EXISTS example, is called a correlated subquery; DuckDB evaluates such subqueries efficiently and often flattens them into a join.

Common Table Expressions

A CTE gives an intermediate result a name via WITH, which reads left to right instead of inside out:

WITH dept_avg AS (
    SELECT dept, avg(salary) AS avg_sal, count(*) AS heads
    FROM employees
    GROUP BY dept
)
SELECT e.name, e.dept, e.salary, d.avg_sal,
       e.salary - d.avg_sal AS gap
FROM employees e
JOIN dept_avg d USING (dept)
WHERE e.salary > d.avg_sal;

Both a derived table and a CTE produce an equivalent plan here; the CTE version is simply easier to maintain as the number of steps grows.

Multiple and Chained CTEs

One WITH can define several CTEs separated by commas, and later CTEs may reference earlier ones, building a pipeline of stages:

WITH monthly AS (
    SELECT date_trunc('month', order_date) AS m, sum(amount) AS rev
    FROM orders GROUP BY 1
),
ranked AS (
    SELECT m, rev, rank() OVER (ORDER BY rev DESC) AS rnk
    FROM monthly
)
SELECT m, rev FROM ranked WHERE rnk <= 3;

Recursive CTEs

Adding the RECURSIVE keyword lets a CTE reference itself, which expresses hierarchical walks such as an employee tree or a bill of materials:

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

The base case seeds the result, and the recursive step repeatedly joins back onto the CTE until no new rows appear.

Choosing Between Them

Use subqueries for one-off filters such as IN, EXISTS or a single scalar lookup, and CTEs whenever an intermediate result is named, reused or ordered as a pipeline step. DuckDB materializes or inlines CTEs behind the scenes, so choosing the clearer form rarely costs performance.

Summary

Subqueries express nested logic in three placements, scalar, set-based and as derived tables, while CTEs under WITH turn multi-stage analyses into a labeled pipeline that can chain and even recurse with RECURSIVE. Together they keep analytical SQL modular and readable, and DuckDB optimizes both forms aggressively so readability carries no runtime penalty.