Recursive CTEs for Hierarchical Data in Laravel | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com) [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles  ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

  [ home ](https://www.msaied.com)    [ articles ](https://www.msaied.com/articles)    Recursive CTEs and Hierarchical Data in Laravel with PostgreSQL        On this page       1. [  The Problem With Nested Sets and Closure Tables ](#the-problem-with-nested-sets-and-closure-tables)
2. [  The Schema ](#the-schema)
3. [  Writing the Recursive CTE ](#writing-the-recursive-cte)
4. [  Integrating With Eloquent ](#integrating-with-eloquent)
5. [  Fetching Ancestors (Upward Traversal) ](#fetching-ancestors-upward-traversal)
6. [  Guarding Against Infinite Loops ](#guarding-against-infinite-loops)
7. [  Performance Notes ](#performance-notes)
8. [  Takeaways ](#takeaways)

  ![Recursive CTEs and Hierarchical Data in Laravel with PostgreSQL](https://cdn.msaied.com/607/ac508dd27011f0f2c57b0bee7707b740.png)

  #laravel   #postgresql   #eloquent   #database  

 Recursive CTEs and Hierarchical Data in Laravel with PostgreSQL 
=================================================================

     29 Aug 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   The Problem With Nested Sets and Closure Tables  ](#the-problem-with-nested-sets-and-closure-tables)
2. [  02   The Schema  ](#the-schema)
3. [  03   Writing the Recursive CTE  ](#writing-the-recursive-cte)
4. [  04   Integrating With Eloquent  ](#integrating-with-eloquent)
5. [  05   Fetching Ancestors (Upward Traversal)  ](#fetching-ancestors-upward-traversal)
6. [  06   Guarding Against Infinite Loops  ](#guarding-against-infinite-loops)
7. [  07   Performance Notes  ](#performance-notes)
8. [  08   Takeaways  ](#takeaways)

 The Problem With Nested Sets and Closure Tables
-----------------------------------------------

Most Laravel tutorials reach for nested sets or closure tables when modelling hierarchical data. Both work, but they add write complexity: every insert or move must update auxiliary columns or rows. PostgreSQL's `WITH RECURSIVE` gives you the same read power from a plain adjacency-list table—just a `parent_id` foreign key—with no extra bookkeeping.

### The Schema

```sql
CREATE TABLE categories (
    id         BIGSERIAL PRIMARY KEY,
    parent_id  BIGINT REFERENCES categories(id) ON DELETE CASCADE,
    name       TEXT NOT NULL
);

CREATE INDEX idx_categories_parent ON categories(parent_id);

```

In Laravel the migration is straightforward:

```php
Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->foreignId('parent_id')->nullable()->constrained('categories')->cascadeOnDelete();
    $table->string('name');
});

```

### Writing the Recursive CTE

A recursive CTE has two parts joined by `UNION ALL`: the **anchor** (the starting row) and the **recursive member** (the self-join that walks the tree).

```sql
WITH RECURSIVE subtree AS (
    -- anchor: the root we care about
    SELECT id, parent_id, name, 0 AS depth
    FROM   categories
    WHERE  id = :root_id

    UNION ALL

    -- recursive member: children of the current frontier
    SELECT c.id, c.parent_id, c.name, s.depth + 1
    FROM   categories c
    JOIN   subtree s ON c.parent_id = s.id
)
SELECT * FROM subtree ORDER BY depth, name;

```

PostgreSQL iterates until no new rows are produced, so you get the full subtree in one round-trip.

### Integrating With Eloquent

The cleanest approach is a **local scope** that swaps the base query for the CTE result:

```php
class Category extends Model
{
    public function scopeSubtreeOf(Builder $query, int $rootId): Builder
    {
        $sql = addBinding($rootId, 'from')
            ->orderBy('depth')
            ->orderBy('name');
    }
}

```

Usage is ergonomic:

```php
$tree = Category::subtreeOf(42)->get();

```

Because `fromSub` replaces the `FROM` clause, all subsequent Eloquent constraints (`where`, `with`, `select`) still compose correctly.

### Fetching Ancestors (Upward Traversal)

Flip the join direction to walk toward the root:

```php
public function scopeAncestorsOf(Builder $query, int $leafId): Builder
{
    $sql = addBinding($leafId, 'from')
        ->orderByDesc('depth');
}

```

This is ideal for breadcrumb generation: `Category::ancestorsOf($currentId)->pluck('name')` returns the path from root to leaf.

### Guarding Against Infinite Loops

PostgreSQL stops when the recursive member returns zero rows, but a corrupted `parent_id` cycle will loop forever. Add a depth guard:

```sql
WHERE s.depth < 50  -- inside the recursive member's WHERE clause

```

Or use the `CYCLE` clause available in PostgreSQL 14+:

```sql
WITH RECURSIVE subtree AS ( ... )
CYCLE id SET is_cycle USING path
SELECT * FROM subtree WHERE NOT is_cycle;

```

### Performance Notes

- The index on `parent_id` is critical; PostgreSQL uses it on every recursive iteration.
- For very wide trees (thousands of siblings per level), add a composite index `(parent_id, name)` to cover the `ORDER BY`.
- `EXPLAIN (ANALYZE, BUFFERS)` will show a `CTE Scan` node; ensure it reads from the index rather than a sequential scan on the base table.

Takeaways
---------

- A plain adjacency-list table plus `WITH RECURSIVE` handles most tree use-cases without closure tables or nested sets.
- Wrap the CTE in a `fromSub` scope so Eloquent constraints remain composable.
- Walk downward for subtrees, upward for breadcrumbs—same pattern, reversed join.
- Add a depth guard or PostgreSQL 14's `CYCLE` clause to protect against corrupt data.
- Index `parent_id` (and optionally cover with sort columns) to keep recursive iterations fast.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Frecursive-ctes-and-hierarchical-data-in-laravel-with-postgresql&text=Recursive+CTEs+and+Hierarchical+Data+in+Laravel+with+PostgreSQL) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Frecursive-ctes-and-hierarchical-data-in-laravel-with-postgresql) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Can I eager-load relationships on the result of a recursive CTE scope?        Yes. Because the scope uses `fromSub` to replace the FROM clause rather than wrapping the entire query, Eloquent's `with()` calls still append the standard relationship sub-queries. Just chain `-&gt;with('products')` as normal after `subtreeOf()`. 

      Q02  Is `WITH RECURSIVE` significantly slower than a closure table for reads?        For moderate tree depths (under ~20 levels) and a proper index on `parent_id`, the difference is negligible. Closure tables win on very deep trees with millions of rows because they trade write cost for a flat read. Profile with `EXPLAIN ANALYZE` for your specific data shape before optimising prematurely. 

      Q03  Does this approach work with MySQL?        MySQL 8.0+ supports `WITH RECURSIVE`, so the SQL is portable. However, the `CYCLE` detection clause is PostgreSQL-specific. On MySQL you must rely on a depth guard in the WHERE clause of the recursive member. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://www.msaied.com/articles) 

 [ ![Laravel API Resources: Sparse Fieldsets, Cursor Pagination, and Per-Route Rate Limiting](https://cdn.msaied.com/606/93349b03f4527b9100157c6774bb4ce2.png) laravel api eloquent 

### Laravel API Resources: Sparse Fieldsets, Cursor Pagination, and Per-Route Rate Limiting

Go beyond basic JsonResource usage. This guide covers sparse fieldsets, cursor-based pagination for large data...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 29 Aug 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-api-resources-sparse-fieldsets-cursor-pagination-and-per-route-rate-limiting) [ ![Laravel Starter Kits Now Ship with Vite+](https://cdn.msaied.com/604/ff70a112664fcb8b68719ab94a842145.png) Laravel Vite+ Starter Kits 

### Laravel Starter Kits Now Ship with Vite+

All Laravel starter kits now use Vite+, the unified toolchain that replaces ESLint and Prettier with Oxlint an...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 28 Aug 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-starter-kits-now-ship-with-vite) [ ![Laravel Queues at Scale: Backpressure, Dead-Letter Queues, and Graceful Degradation](https://cdn.msaied.com/602/fcffaaa5442f84486d6059eaa4106d26.png) laravel queues reliability 

### Laravel Queues at Scale: Backpressure, Dead-Letter Queues, and Graceful Degradation

Beyond basic queue workers: learn how to implement backpressure signals, dead-letter queues, and graceful degr...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 28 Aug 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-queues-at-scale-backpressure-dead-letter-queues-and-graceful-degradation) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [Contact](https://www.msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
