优化Laravel 9.x无限级分类性能的关键是分场景选模型、控查询范围、减数据体积、准缓存管理:≤5层用邻接表+预加载;>5层且读多写少用nestedset;禁用无限with嵌套,改用CTE查ID再批量获取;缓存需带版本号与主动失效机制。

优化 Laravel 9.x 深度层级无限级分类查询性能,关键不是“怎么查得更深”,而是“怎么避免查得慢又不稳”。邻接表(parent_id)模型天然不适合深度遍历,硬扛递归或嵌套 with 容易触发 N+1、内存溢出或 MySQL 堆栈超限。真正有效的策略是分场景选模型、控查询范围、减数据体积、准缓存管理。
优先评估是否真需“无限深度”
多数业务中所谓“无限级”,实际有效深度 rarely 超过 4–6 层(如:类目 → 大类 → 中类 → 小类 → 品牌 → SKU)。若确定不会超过 5 层,用邻接表 + 预加载即可,无需上复杂方案:
- 定义好
parent_id允许 null,并加索引:$table->index('parent_id'); - 查根节点及全部子树(≤3 层):
Category::whereNull('parent_id')->with(['children' => fn($q) => $q->with('children')])->get(); - 查某节点下全部后代(≤4 层):
Category::where('id', $id)->with(['children' => fn($q) => $q->with(['children' => fn($q2) => $q2->with('children')])])->first();
深度 > 5 层且读多写少?换 nestedset 扩展包
kalnoy/nestedset 使用左右值(_lft/_rgt)模型,单次查询即可获取整棵子树或路径,适合后台类目管理、前台静态导航等场景:
- 安装:
composer require kalnoy/nestedset - 迁移中调用
NestedSet::columns($table),自动生成_lft、_rgt、parent_id - 查某节点所有后代:
$node->descendants;(一条 SELECT,无递归) - 查某节点完整路径:
$node->ancestors;(同样高效) - 注意:插入/移动节点会重算左右值,写操作略重,不适合高频变动的树
拒绝 N+1,也别滥用 with('relation') 无限嵌套
with('children.children.children...') 看似简洁,但 Eloquent 不会自动截断层级,容易生成冗余查询或空集合爆炸。更稳的做法是“两步走”:
- 第一步:用原生递归 CTE 查出所有相关 ID(MySQL 8.0+ 或 PostgreSQL 支持):
DB::select("WITH RECURSIVE tree AS (SELECT id, parent_id FROM categories WHERE id = ? UNION ALL SELECT c.id, c.parent_id FROM categories c INNER JOIN tree t ON c.parent_id = t.id) SELECT id FROM tree", [$rootId]); - 第二步:用这些 ID 一次查出全部节点:
Category::whereIn('id', $allIds)->get(); - 最后在 PHP 层用
collect()->groupBy('parent_id')构建树结构——轻量、可控、无深度限制
缓存必须带层级语义和主动失效
分类树缓存一旦污染,整个导航就错乱。不能只缓存一个键,也不能只靠 TTL:
- 键名含业务维度,例如:
"category:tree:root_{$rootId}:v3",版本号 v3 便于全量刷新 - 对每个节点变更(新增/移动/删除),清除其父节点缓存:
Cache::forget("category:tree:root_{$parentId}:v3"); - 用 Redis tags 分组管理:
Cache::tags(['category-tree', "root-{$rootId}"])->remember(...),删时可精准批量清理 - 根节点缓存 TTL 设为 1 小时,子节点继承;但用户编辑后立即
forget(),不等过期











