$ lexprog.com

// notes from an old coder -- php, databases, and the occasional rant

[December 30, 2025] PostgreSQL

PostgreSQL CTEs: Recursive Queries

PostgreSQL CTEs: Recursive Queries

────────────────────────────────────────────────────────

PostgreSQL CTEs: Recursive Queries

Tip: Basic CTE

WITH active_users AS (
    SELECT id, name FROM users WHERE active = true
)
SELECT * FROM posts WHERE user_id IN (SELECT id FROM active_users);

Makes complex queries readable.

Gotcha: CTEs Are Optimization Fences

PostgreSQL materializes CTEs — they run once and store results. This can be slower than a subquery for simple cases.

Tip: Recursive CTE for Hierarchies

WITH RECURSIVE tree AS (
    SELECT id, name, parent_id, 0 as level
    FROM categories WHERE parent_id IS NULL
    UNION ALL
    SELECT c.id, c.name, c.parent_id, t.level + 1
    FROM categories c JOIN tree t ON c.parent_id = t.id
)
SELECT * FROM tree ORDER BY level;

Traverses unlimited-depth trees.

Gotcha: Infinite Loops in Recursive CTEs

If your data has circular references, the recursive CTE runs forever. Add a depth limit.

Tip: Multiple CTEs in One Query

WITH a AS (...), b AS (...), c AS (...)
SELECT * FROM a JOIN b ON ... JOIN c ON ...;

Chain multiple CTEs for complex data pipelines.

Gotcha: MATERIALIZED Hint (PG 12+)

WITH a AS MATERIALIZED (...)

Forces materialization. Without it, PostgreSQL may inline the CTE.

Tip: EXPLAIN (ANALYZE, BUFFERS) Is Your Best Friend

For query debugging, always use EXPLAIN (ANALYZE, BUFFERS) instead of plain EXPLAIN. The BUFFERS option shows hit/miss rates for every node, revealing whether your indexes are actually in memory.

Tip: Partial Indexes Are Underutilized

CREATE INDEX ON orders (status) WHERE status = 'pending' creates a tiny index that covers only the rows your query needs. It's faster to scan and cheaper to maintain than a full-column index.

Gotcha: NULL Sorting Is Non-Obvious

By default, NULLs sort AFTER non-null values in ascending order. ORDER BY col DESC puts NULLs FIRST. Use NULLS LAST or NULLS FIRST to be explicit.

Senior Insight

Common Table Expressions (CTEs) are PostgreSQL's answer to readable complex queries. I've used recursive CTEs to query hierarchical data (category trees, org charts) that would require multiple queries with Eloquent. But CTEs are optimization fences in PostgreSQL — the planner treats each CTE as a separate optimization unit, which can prevent join elimination. For performance-critical queries, test with and without CTE inlining (PostgreSQL 12+ inlines CTEs automatically).

Source: pganalyze Blog (https://pganalyze.com/blog), PostgreSQL Docs (https://www.postgresql.org/docs/current/), Crunchy Data Blog (https://www.crunchydata.com/blog)

────────────────────────────────────────────────────────
<-- back to posts