
本文介绍如何在 laravel 5.2.45 中替代嵌套循环,通过数据库递归查询(mysql 8.0+ cte)或内存级递归算法,将获取完整家族树的时间复杂度从 o(n²) 降至 o(n),显著提升大数据量下的层级遍历性能。
在 Laravel 5.2 应用中处理树形结构(如用户上下级关系)时,原始代码采用「全表预加载 + 多层 foreach 嵌套」的方式获取某节点的所有后代,虽逻辑直观,但存在严重性能瓶颈:
- 每次调用 User::get() 加载全部用户,时间复杂度为 O(n);
- 后续四层手动遍历依赖 $cache 数组查找,最坏情况达 O(n²),且硬编码层级(仅支持最多 4 层),扩展性差;
- 内存占用高,无法利用数据库索引与优化器。
✅ 推荐方案一:MySQL 8.0+ 递归 CTE(最优解)
若数据库版本 ≥ 8.0,直接使用 WITH RECURSIVE CTE 查询祖先链路(如查找用户 E 的所有上级),语句简洁、执行高效、由数据库引擎原生优化:
WITH RECURSIVE user_ancestors AS (
-- 锚点:起始节点(例如 id = 5 对应用户 E)
SELECT id, name, parent_id
FROM users
WHERE id = 5
UNION ALL
-- 递归:向上追溯 parent_id 对应的记录
SELECT u.id, u.name, u.parent_id
FROM users u
INNER JOIN user_ancestors ua ON u.id = ua.parent_id
)
SELECT id, name, parent_id FROM user_ancestors;该查询返回结果为:
| id | name | parent_id |
|----|------|-----------|
| 5 | E | 4 |
| 4 | D | 3 |
| 3 | C | 2 |
| 2 | B | 1 |
| 1 | A | 0 |
? 注意:此 CTE 获取的是 向上祖先(即 E 的所有上级),若需 向下子孙(如 A 的所有后代),只需交换 JOIN 条件为 ua.id = u.parent_id,并从根节点(如 WHERE id = 1)启动递归。
在 Laravel 中安全调用(使用 DB::select() 防止注入):
use Illuminate\Support\Facades\DB;
public function getAncestors($userId)
{
$sql = "WITH RECURSIVE user_ancestors AS (
SELECT id, name, parent_id FROM users WHERE id = ?
UNION ALL
SELECT u.id, u.name, u.parent_id
FROM users u
INNER JOIN user_ancestors ua ON u.id = ua.parent_id
)
SELECT * FROM user_ancestors";
return DB::select($sql, [$userId]);
}✅ 优势:单次查询、索引友好、无 PHP 循环开销、天然支持任意深度。
✅ 推荐方案二:PHP 层递归 + 一次查询(兼容低版本 MySQL)
若数据库不支持 CTE(如 MySQL 5.7),可改用「一次查全表 + 内存递归」策略,避免多层嵌套:
public function getAllDescendants($parentId)
{
// 一次性获取全部用户,并按 parent_id 建立索引映射
$allUsers = User::pluck('id', 'parent_id')->mapWithKeys(function ($id, $pid) {
return [$pid => User::where('parent_id', $pid)->get()->keyBy('id')->toArray()];
})->filter()->all();
$result = [];
$this->collectDescendants($parentId, $allUsers, $result);
return collect($result)->keyBy('id')->values();
}
protected function collectDescendants($parentId, $cache, &$result)
{
if (empty($cache[$parentId])) {
return;
}
foreach ($cache[$parentId] as $child) {
$result[] = $child;
$this->collectDescendants($child['id'], $cache, $result); // 递归深入
}
}⚠️ 注意事项:
- pluck('id', 'parent_id') 仅用于快速判断是否存在子节点,真实数据仍需 where('parent_id', ...) 查询;
- 实际项目中建议缓存 $cache 结构(如 Redis),避免高频重复构建;
- 递归深度过大时需检查 PHP xdebug.max_nesting_level 配置。
? 总结与选型建议
| 方案 | 适用场景 | 时间复杂度 | 维护成本 | 数据库要求 |
|---|---|---|---|---|
| MySQL CTE | MySQL ≥ 8.0,需高性能 & 深度不确定 | O(n) | 低(SQL 简洁) | ✅ 必须 |
| PHP 递归 + 单次全表 | 兼容旧版 MySQL,数据量 | O(n) | 中(需管理递归逻辑) | ❌ 无要求 |
| 原始嵌套循环 | 仅作教学参考,生产环境禁用 | O(n²) | 高(难扩展、易出错) | ❌ 不推荐 |
? 核心原则:把层级遍历交给数据库(CTE)或至少保证数据只查一次,杜绝 N+1 和重复扫描。 在 Laravel 5.2 中,优先升级数据库或采用方案二重构,即可彻底解决原文本中“四层硬编码 + 全表遍历”的性能顽疾。










