Using Common Table Expressions
1. Creating Basic CTE
WITH high_value AS (
SELECT customer_id, sum(total_cents) AS total
FROM orders GROUP BY customer_id HAVING sum(total_cents) > 100000
)
SELECT c.full_name, h.total FROM high_value h JOIN customers c ON c.id = h.customer_id;
2. Using Multiple CTEs
WITH
recent AS (SELECT * FROM orders WHERE placed_at >= now()-interval '30 days'),
per_customer AS (SELECT customer_id, count(*) AS n FROM recent GROUP BY customer_id)
SELECT * FROM per_customer ORDER BY n DESC;
3. Referencing CTEs in Queries
| Rule | Detail |
|---|---|
| Forward only | Each CTE can reference earlier CTEs |
| Scope | Same statement only |
| Reuse | Multiple references OK |
4. Creating Recursive CTE
WITH RECURSIVE numbers(n) AS (
SELECT 1
UNION ALL
SELECT n+1 FROM numbers WHERE n < 10
)
SELECT * FROM numbers;
5. Using Recursive CTE for Hierarchical Data
WITH RECURSIVE org AS (
SELECT id, name, manager_id, 1 AS depth, ARRAY[id] AS path
FROM employees WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, o.depth+1, o.path || e.id
FROM employees e JOIN org o ON e.manager_id = o.id
)
SELECT * FROM org ORDER BY path;
6. Using Recursive CTE for Graph Traversal
WITH RECURSIVE reachable AS (
SELECT src, dst FROM edges WHERE src = :start
UNION
SELECT r.src, e.dst
FROM reachable r JOIN edges e ON e.src = r.dst
)
SELECT DISTINCT dst FROM reachable;
Note: Use
UNION (not UNION ALL) when you need cycle elimination, or use CYCLE clause (PG 14+).7. Using CTE with INSERT Statement
WITH new_rows AS (
INSERT INTO orders (customer_id, total_cents) VALUES (42, 1999)
RETURNING *
)
INSERT INTO audit (order_id, action) SELECT id, 'created' FROM new_rows;
8. Using CTE with UPDATE Statement
WITH updated AS (
UPDATE orders SET status='shipped'
WHERE status='paid' AND placed_at < now()-interval '1 day'
RETURNING id
)
SELECT count(*) FROM updated;
9. Using CTE with DELETE Statement
WITH purged AS (
DELETE FROM events WHERE created_at < now()-interval '90 days' RETURNING id
)
INSERT INTO purged_log SELECT id, now() FROM purged;
10. Using MATERIALIZED Hint
WITH q AS MATERIALIZED (
SELECT * FROM big_table WHERE expensive_predicate
)
SELECT * FROM q WHERE q.x = 1 UNION ALL SELECT * FROM q WHERE q.x = 2;
| Mode | Behavior |
|---|---|
| MATERIALIZED | Compute once, fence optimizer |
| NOT MATERIALIZED | Inline (default if single reference) |
| Default (PG 12+) | Inline if referenced once and no side effects |
11. Using NOT MATERIALIZED Hint
WITH q AS NOT MATERIALIZED (SELECT id FROM big WHERE active)
SELECT * FROM other JOIN q USING (id); -- inlined for predicate push-down
12. Understanding CTE vs Subquery Performance
| Aspect | Subquery | CTE |
|---|---|---|
| Inlining | Always | Conditional (PG 12+) |
| Predicate pushdown | Yes | Yes if NOT MATERIALIZED |
| Reuse | Re-computed | Cache via MATERIALIZED |
| Readability | Nested | Top-down chain |
13. Using SEARCH Clause in Recursive CTE
WITH RECURSIVE tree AS (
SELECT id, parent_id, name FROM nodes WHERE parent_id IS NULL
UNION ALL
SELECT n.id, n.parent_id, n.name FROM nodes n JOIN tree t ON n.parent_id = t.id
) SEARCH DEPTH FIRST BY id SET ord
SELECT * FROM tree ORDER BY ord;
| Mode | Order |
|---|---|
| DEPTH FIRST | Pre-order DFS |
| BREADTH FIRST | Level-order BFS |
14. Using CYCLE Clause in Recursive CTE
WITH RECURSIVE walk(node, path, is_cycle) AS (
SELECT id, ARRAY[id], false FROM nodes WHERE id = 1
UNION ALL
SELECT n.id, w.path || n.id, n.id = ANY(w.path)
FROM nodes n JOIN walk w ON n.parent_id = w.node
WHERE NOT w.is_cycle
) CYCLE node SET is_cycle USING path
SELECT * FROM walk;