{"post":{"slug":"recursive-ctes-explained-2026","title":"Recursive CTEs: Querying Hierarchies in One Statement","excerpt":"Walking an org chart or a category tree in application code means N queries. A recursive common table expression does it in one, and it needs a termination condition you supply.","category":"code","date":"January 8, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">Y</span>ou need every descendant of a category, or the full management chain above an employee, or all dependencies of a package. The obvious approach queries one level, then queries the next using those results, and repeats.\n\nThat is N round trips for a depth of N, and round trips dominate. A recursive common table expression does the whole traversal inside the database and returns the result once.\n\n## What changed in 2026\n\n- **Support became universal.** Recursive CTEs are available across mainstream databases, so portability concerns largely disappeared.\n- **Cycle safety got better tooling.** Some engines added explicit cycle-detection syntax rather than requiring manual path tracking.\n- **Graph databases stopped being the default answer.** Teams recognised that moderate hierarchical traversal in a relational database is usually adequate.\n- **Depth limits became standard practice.** Capping recursion depth moved from a nice-to-have to something reviewers ask about.\n\n## The structure\n\nA recursive CTE has two parts joined by a union.\n\nThe **anchor** selects the starting rows — the root of the tree, or the node you are querying from.\n\nThe **recursive term** references the CTE itself, selecting rows that connect to what the previous iteration produced. It runs repeatedly until it returns no new rows.\n\nThat final condition is the whole termination story: the query stops when an iteration adds nothing. If your data contains a cycle — a category that is somehow its own ancestor, a dependency loop — every iteration keeps finding rows and the query never stops.\n\n## Guaranteeing termination\n\nTwo safeguards, and you want at least the first.\n\n**A depth counter.** Add a column starting at zero in the anchor and incrementing in the recursive term, with a condition capping it. Every real hierarchy has a sensible maximum depth, and a cap well above it costs nothing and turns an infinite loop into a bounded query.\n\n**Path tracking.** Accumulate the visited node identifiers in an array and exclude rows already present. This detects genuine cycles rather than merely bounding them, at the cost of carrying the array through every row.\n\n| Safeguard | Prevents | Cost |\n| --- | --- | --- |\n| Depth cap | Infinite loops | Negligible |\n| Path array | Cycles specifically | Array per row |\n| Engine cycle syntax | Cycles | Varies; check support |\n| Nothing | — | Query runs forever |\n\nUse the depth cap always. Add path tracking when cycles are genuinely possible in your data rather than merely conceivable.\n\n## Where it costs\n\nA recursive CTE is a loop inside the database, and each iteration is a join against the working set. Performance depends almost entirely on whether that join is indexed.\n\nTraversing a parent-child relationship requires an index on the parent column, or every iteration scans the table. That is the single most common cause of a recursive query being unexpectedly slow, and it is easy to miss because the query looks like one statement rather than a loop — see [database indexing explained](/blog/database-indexing-explained-2026).\n\nBreadth matters as much as depth. A tree with a high branching factor produces a working set that grows quickly, and a traversal from the root of a large hierarchy can return an enormous number of rows. Filtering inside the recursive term, rather than after the CTE, prunes branches before they expand.\n\nFor very deep or very broad traversals run frequently, materialising the closure — precomputing ancestor-descendant pairs in a table — trades write cost for read speed. Worth it only when the read pattern justifies it.\n\n## Common mistakes\n\n- **No depth cap.** A single cyclic row hangs the query.\n- **Missing index on the join column.** Each iteration scans.\n- **Filtering after the CTE rather than inside it.** Expands branches you then discard.\n- **Using it for fixed shallow depth.** Two joins are clearer.\n- **Selecting more columns than needed.** Carried through every iteration.\n- **Assuming a cycle is impossible.** Data quality issues create them.\n- **Not testing on realistic depth and breadth.** Small test data hides the cost.\n\n## FAQ\n\n**How deep can recursion go?**\nEngines have configurable limits, and hitting a limit is a symptom rather than a constraint to raise. If a legitimate traversal is that deep, reconsider the data model.\n\n**Are these portable across databases?**\nBroadly yes for the standard form. Cycle-detection syntax and recursion limits vary, so check those specifically.\n\n**Should I use a graph database instead?**\nFor occasional hierarchical traversal, no — a recursive CTE is adequate and avoids another system. For traversal-heavy workloads with complex path queries, a purpose-built store earns its place.\n\n**How do I make one faster?**\nIndex the join column, filter inside the recursive term, select fewer columns, and consider materialising the closure if the same traversal runs constantly — see [what is a materialized view](/blog/what-is-a-materialized-view-2026).\n\n## Where to go next\n\nFor the indexes recursive traversal depends on, read [database indexing explained](/blog/database-indexing-explained-2026). For precomputing expensive traversals, [what is a materialized view](/blog/what-is-a-materialized-view-2026), and for reading the plan, [query planners](/blog/query-planner-explained-2026).","hue":300,"metaTitle":"Recursive CTEs: Querying Hierarchies in One Statement","metaDescription":"How recursive common table expressions traverse trees and graphs in SQL, why termination must be guaranteed, and how to detect cycles safely.","keywords":["recursive cte","hierarchical query sql","tree traversal sql","with recursive","cycle detection sql","org chart query","graph traversal database","sql patterns 2026"],"tldr":["**One query replaces a loop of queries** — the round trips are usually the real cost.","**You must guarantee termination** — a cycle in the data produces an infinite loop.","**Add a depth column and cap it** — the simplest and most reliable safeguard.","**Track the visited path to detect cycles** — necessary when the data genuinely may contain them.","**Skip:** recursive CTEs for shallow fixed-depth hierarchies. A join or two is clearer."],"readTime":"4 min read","wordCount":833},"related":[{"slug":"what-is-a-directed-acyclic-graph-2026","title":"What Is a Directed Acyclic Graph? 2026 Guide","excerpt":"A directed acyclic graph, or DAG, is a graph whose edges point one way and never loop back on themselves. Here is what that one rule enables and where DAGs quietly run your software.","category":"code","date":"July 17, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":240},{"slug":"what-is-a-graph-data-structure-2026","title":"What Is a Graph Data Structure? 2026 Guide","excerpt":"A graph is nodes plus edges, with no hierarchy required, unlike a tree. This 2026 guide covers directed vs undirected, adjacency lists vs matrices, and how graphs get traversed.","category":"code","date":"July 18, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":240},{"slug":"how-to-profile-slow-code-2026","title":"How to profile slow code in 2026: flamegraphs, tracing, and real fixes","excerpt":"Slow code is rarely where you think it is. This is the 2026 guide to profiling accurately — CPU flamegraphs, memory profiles, and database query analysis.","category":"code","date":"April 17, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":320}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"rank-fusion-explained-2026","title":"Rank Fusion: Combining Search Results That Do Not Compare","excerpt":"Vector similarity and keyword scores live on incomparable scales. Fusing by rank position rather than score sidesteps the problem and needs no tuning.","category":"ai","date":"January 8, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":215},"older":{"slug":"usb-flash-endurance-2026","title":"USB Flash Endurance: Why Cheap Drives Lose Data","excerpt":"Flash memory does not hold data indefinitely without power, and inexpensive drives are worse at it than anyone advertises. Understanding why explains what flash is safe to use for.","category":"tech","date":"January 8, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":233}}