无限级分类需统计全子树关联数据总量,推荐用WITH RECURSIVE查后代ID再whereIn聚合;NestedSet方案适合高频查询;避免误用withCount跨层级统计。

在 Laravel 9.x 中为无限级分类统计子分类关联数据(比如每个分类下有多少文章、商品或子分类数),不能简单套用常规 withCount,因为子分类本身是递归结构——你要的不是“直接子类数量”,而是“整个子树中所有关联记录的总数”。这需要分层处理逻辑和数据库配合。
明确统计目标:直接子类 vs 全子树
先区分两个常见需求:
-
直接子分类数:即
$category->children()->count(),用withCount('children')即可,无需特殊处理; - 全子树关联数据总量:例如「该分类及其所有后代分类下的文章总数」,这才是无限级场景的难点——它涉及跨层级聚合,Eloquent 默认不支持自动下钻统计。
方案一:预加载 ID 树 + 一次性关联统计(推荐)
最稳定、兼容性强、不依赖扩展包的做法。核心思路是:先查出整棵子树所有分类 ID,再用 whereIn 一次性统计关联模型数量。
- 用 MySQL 8.0+ 的
WITH RECURSIVE查询全部后代 ID(含自身):
public function scopeWithDescendantIds($query, $id)
{
return $query->from(DB::raw("(WITH RECURSIVE tree AS (
SELECT id FROM categories WHERE id = {$id}
UNION ALL
SELECT c.id FROM categories c INNER JOIN tree t ON c.parent_id = t.id
) SELECT id FROM tree) as descendant_ids"));
}
- 控制器中组合使用:
$descendantIds = Category::withDescendantIds($categoryId)->pluck('id');
$articleCount = Article::whereIn('category_id', $descendantIds)->count();
- 若需多个关联统计(如文章数、商品数),可统一用
DB::table(...)->whereIn(...)->selectRaw('COUNT(*) as total')->groupBy('...')合并查询,避免 N+1。
方案二:借助 NestedSet 扩展(适合高频深度查询)
如果你已使用 kalnoy/nestedset 或类似包(Laravel 9.x 完全兼容),它的 _lft/_rgt 字段天然支持子树范围查询:
- 查某分类子树内所有文章总数:
$category = Category::findOrFail($id);
$articleCount = Article::whereHas('category', function ($q) use ($category) {
$q->whereBetween('lft', [$category->lft, $category->rgt]);
})->count();
- 优势:单次 WHERE 范围查询,无递归开销;适合后台数据看板、权限校验等读多写少场景;
- 注意:插入/移动节点时需维护左右值,比邻接表稍重,但 Laravel 9.x 的
NestedSet服务已封装好事务安全操作。
方案三:避免踩坑的 withCount 误用提醒
不要对递归关系滥用 withCount,例如:
-
Category::withCount(['children' => fn($q) => $q->withCount('articles')])—— 这只会统计「直接子类各自的 article 数」,不是整棵树总和; -
Category::withCount(['children.articles'])语法错误,Eloquent 不支持跨两级关系的 withCount; - 想按子树聚合?必须跳出 withCount 思维,改用上述 ID 集合或 NestedSet 方式做外层聚合。











