最常用方式是查出全部栏目后用PHP组装树形结构,关键在于提前构建父子映射并用引用挂载子树,避免N+1查询和重复遍历,兼顾性能与可控性。

PHP递归查询数据库生成栏目树的常见写法
直接从数据库查出全部栏目再用PHP组装树形结构,是最常用也最可控的方式。关键不是“能不能”,而是“怎么组织数据才不掉坑”。
假设表结构为 id、name、parent_id,且根节点 parent_id = 0 或 NULL(需统一):
- 先用一次
SELECT * FROM category ORDER BY parent_id, sort ASC拿到全部数据,避免N+1查询 - 用
parent_id做键,把所有子节点归组到一个$map数组里:$map[$row['parent_id']][] = $row -
递归函数只负责拼装层级,不查库;入口从
$map[0](或$map[NULL])开始
注意:如果 parent_id 允许为 NULL,别用 0 当作根判断条件,否则 isset($map[0]) 会误判。
用引用方式一次性构建树,避免重复遍历
递归调用本身没问题,但若每次都在全量数组里 array_filter 找子项,性能会随层级和数量陡增。更稳的做法是提前建好父子映射,再用引用“挂载”子树。
立即学习“PHP免费学习笔记(深入)”;
核心逻辑是:遍历原始数组时,对每个节点,把它塞进其父节点的 children 数组中;根节点直接进结果集。这要求原始数据已按 parent_id 排序,或至少保证父节点在子节点前被处理(可先按 id 升序查出,再用 usort 调整顺序)。
- 初始化空数组
$tree和$refs(用于存每个id对应的引用) - 循环每条记录:
$refs[$row['id']] = &$row,然后if ($row['parent_id'] && isset($refs[$row['parent_id']])) { $refs[$row['parent_id']]['children'][] = &$row; } - 最后遍历原始数组,把
parent_id为空的节点推入$tree
这种方式没有递归调用开销,也不依赖函数栈深度,适合几百个节点以内的栏目结构。
MySQL 8.0+ 用 WITH RECURSIVE 直接查出树形结果
如果数据库是 MySQL 8.0+ 或 PostgreSQL,能用 WITH RECURSIVE 一次性查出带层级和路径的扁平结果,PHP 层只需简单分组,甚至不用递归。
例如 MySQL 查询:
WITH RECURSIVE tree AS ( SELECT id, name, parent_id, 0 AS level, CAST(id AS CHAR) AS path FROM category WHERE parent_id = 0 UNION ALL SELECT c.id, c.name, c.parent_id, t.level + 1, CONCAT(t.path, '-', c.id) FROM category c INNER JOIN tree t ON c.parent_id = t.id ) SELECT * FROM tree ORDER BY path;
返回结果已按树序排列,PHP 只需按 level 缩进或用栈维护当前父级即可生成嵌套数组。缺点是无法在低版本 MySQL 使用,且复杂查询可能影响缓存效率。
容易被忽略的边界情况
真实业务里,栏目树常踩的不是语法坑,而是数据逻辑坑:
-
parent_id指向了不存在的id(孤儿节点),会导致某层“消失”或无限递归(若没加深度限制) - 存在循环引用,比如 A → B → C → A,纯 PHP 递归会爆栈,必须加
$visited集合或层级计数器截断 -
前端需要展开/折叠状态,但后端返回的是完整树,建议加
has_children字段(通过EXISTS子查询或预聚合),而不是让前端猜 -
多语言或站点隔离场景下,
category表常带site_id或lang字段,漏加 WHERE 条件会导致树错乱
树形结构看着简单,真正稳定运行靠的是对数据一致性的预判,而不是递归写得有多漂亮。











