الدورات
title
Building a Flexible Tree Structure in PostgreSQL Using LTREE and Implementing It in Laravel (Full Practical Example)
1. Introduction: Why Use LTREE?
Many applications require hierarchical (tree-like) data:
- LMS: Grade → Subject → Chapter → Lesson
- E-commerce: Category → Subcategory → Product
- Blogs: Topic → Subtopic → Article
Traditionally, developers use parent_id.
But queries like:
- Get all children
- Get all ancestors
- Get full path
- Reorder tree
…become slow and complicated with large datasets.
PostgreSQL LTREE solves this perfectly by storing hierarchical paths directly inside the database.
2. What Is LTREE in PostgreSQL?
ltree is a PostgreSQL data type designed to store label trees, such as:
rootroot.webroot.web.phproot.web.php.laravel
Each level is separated by a dot (.).
LTREE supports powerful operators:
OperatorMeaningpath @> subpathpath is an ancestor of subpathpath <@ superpathpath is a descendant of superpathpath ~ '*.php'wildcard pattern matching
This makes hierarchical queries extremely fast.
3. Enable LTREE in PostgreSQL
Run:
CREATE EXTENSION IF NOT EXISTS ltree;
You must run this once per database.
4. Creating a Categories Table Using LTREE
Example: hierarchical LMS categories such as:
lmslms.programminglms.programming.phplms.programming.php.laravel
4.1 Laravel Migration
id();
$table->string('name');
$table->string('slug')->unique();
$table->unsignedBigInteger('parent_id')->nullable();
$table->timestamps();
});
DB::statement('ALTER TABLE categories ADD COLUMN path ltree');
DB::statement('CREATE INDEX categories_path_gist ON categories USING GIST (path);');
}
public function down()
{
Schema::dropIfExists('categories');
}
};
Notes:
- Creating a GIST index speeds up hierarchical queries.
parent_idis optional but helpful for simple tree operations.
5. Category Model in Laravel
belongsTo(self::class, 'parent_id');
}
public function children()
{
return $this->hasMany(self::class, 'parent_id');
}
}
path is treated as a string in Laravel, which is fine.
6. Building the LTREE Path Logic (Service Layer)
The path is built as:
Root category:
slug
Child category:
parent.path . '.' . slug
CategoryService.php
name = $data['name'];
$category->slug = $slug;
$category->parent_id = $data['parent_id'] ?? null;
$category->save();
// Build LTREE path
if ($category->parent_id) {
$parent = Category::findOrFail($category->parent_id);
$category->path = $parent->path . '.' . $slug;
} else {
$category->path = $slug;
}
$category->save();
return $category;
}
public function update(Category $category, array $data): Category
{
if (isset($data['name'])) $category->name = $data['name'];
if (isset($data['slug'])) $category->slug = $data['slug'];
if (array_key_exists('parent_id', $data)) $category->parent_id = $data['parent_id'];
$category->save();
$this->rebuildPath($category);
return $category;
}
public function rebuildPath(Category $category)
{
if ($category->parent_id) {
$parent = Category::findOrFail($category->parent_id);
$category->path = $parent->path . '.' . $category->slug;
} else {
$category->path = $category->slug;
}
$category->save();
foreach ($category->children as $child) {
$this->rebuildPath($child);
}
}
}
7. Category Controller (REST API)
get();
}
public function store(Request $request)
{
$data = $request->validate([
'name' => 'required|string',
'slug' => 'nullable|string',
'parent_id' => 'nullable|exists:categories,id',
]);
$category = $this->service->create($data);
return response()->json([
'message' => 'Category created',
'item' => $category
], 201);
}
}
8. LTREE Queries in Laravel
8.1 Get all descendants (subtree)
$descendants = Category::whereRaw('path <@ ?::ltree', [$category->path])
->orderBy('path')
->get();
8.2 Get all ancestors (breadcrumb)
$ancestors = Category::whereRaw('?::ltree @> path', [$category->path])
->orderBy('path')
->get();
8.3 Get all categories under a branch
Category::whereRaw('path <@ ?::ltree', ['lms.programming'])->get();
9. Real LMS Example
Assume your LMS has:
lmsprogrammingphplaraveljavascriptvuemathalgebra
LTREE paths:
lmslms.programminglms.programming.phplms.programming.php.laravellms.programming.javascriptlms.programming.javascript.vuelms.mathlms.math.algebra
With LTREE you can:
- Fetch all programming subjects:
-
path <@ 'lms.programming' - Get breadcrumb for Laravel:
- ancestors of
'lms.programming.php.laravel' - Display nested tree simply by ordering by path.
10. Advanced Improvements
Custom Eloquent Scopes
public function scopeDescendantsOf($query, Category $category)
{
return $query->whereRaw('path <@ ?::ltree', [$category->path]);
}
public function scopeAncestorsOf($query, Category $category)
{
return $query->whereRaw('?::ltree @> path', [$category->path]);
}
Usage:
Category::descendantsOf($cat)->get(); Category::ancestorsOf($cat)->get();
Final Summary
Using PostgreSQL LTREE with Laravel gives you:
- Clean and elegant hierarchical data modeling
- Extremely fast tree queries
- Full control of ancestors, descendants, branches, and depth
- Perfect fit for LMS, e-commerce, and CMS systems