应避免递归查库,改用单次查询+内存递归构建树;需确保引用初始化再挂载,根节点单独判断;MySQL 8.0+推荐用WITH RECURSIVE查子ID列表;注意parent_id有效性、sort字段类型及path索引。

getSubcategories 这类纯递归查库函数在数据量稍大时会直接拖垮数据库,不是“怎么写”,而是“不该这么写”。
用一次查询 + 内存递归代替 N+1 查询
每次递归都执行 SELECT * FROM categories WHERE parent_id = ?,层级深一点就触发 MySQL 连接池耗尽或 PHP 栈溢出(Fatal error: Maximum function nesting level of '256' reached)。真实场景中,分类表通常不到 1000 行,全量读入内存再处理,比反复连库快一个数量级。
实操建议:
- 单次查全:
SELECT id, name, parent_id, sort FROM categories WHERE status = 1 ORDER BY parent_id, sort - 转成以
id为键的数组:$list[$row['id']] = $row - 构建父子映射:
$map[$row['parent_id']][] = $row,后续递归只查$map[$parentId],不遍历全量 - 入口函数只传
$map和起始$parentId,彻底隔离数据库调用
避免引用挂载失败导致 children 为空
用 listToTree 或手写引用挂载时,常见现象是:返回的树里有根节点,但所有 children 都是空数组。根本原因不是逻辑错,而是 $ref[$parentId] 还没初始化就被写入 —— 比如某条记录 parent_id = 5 出现在 id = 5 的记录之前。
立即学习“PHP免费学习笔记(深入)”;
实操建议:
- 第一遍遍历:确保每个
$ref[$item['id']] = &$item都被显式赋值(哪怕只是空数组) - 第二遍遍历:再做挂载:
$ref[$item['parent_id']]['children'][] = &$ref[$item['id']] - 根节点必须单独判断:
if (empty($item['parent_id']) || $item['parent_id'] == 0)才加入$tree - 不要依赖
array_reduce一行搞定,它无法保证执行顺序
MySQL 8.0+ 直接用 WITH RECURSIVE 查子 ID 列表
如果只要子节点 ID(比如用于 WHERE IN 查询权限),别在 PHP 做树形展开。CTE 是最干净的解法,天然带层级、可排序、能分页,且不占 PHP 内存。
注意点:
- SQL 中基础查询必须是
WHERE id = ?(查自身),不是WHERE parent_id = ?(漏掉自己) -
UNION ALL后的 JOIN 要走parent_id索引,否则性能反不如 PHP 内存遍历 - PDO 绑定参数时,不能把整个 CTE 当字符串拼接,要用原生预处理
- 返回结果默认无序,加
ORDER BY id或ORDER BY depth显式控制
真正卡住人的从来不是“怎么写出递归”,而是 parent_id 指向了不存在的记录、sort 字段类型是字符串导致排序错乱、或者忘了给 path 字段加索引就敢上 LIKE 查询——这些细节不检查,树建得再漂亮也查不出数据。



















