优先使用CTE而非withTree(),因CTE在数据库内完成递归,单次查询、天然有序、可限深度和分页,避免PHP内存构树的性能瓶颈与N+1风险;withTree()需全量查后PHP递归,无层级控制且深层级时耗时显著上升。

ThinkPHP 6.0 原生不支持数据库层递归查询(如 MySQL 的 CTE),但若你使用 MySQL 8.0+ 或 PostgreSQL,可绕过 TP 的 ORM 层,直接用原生 CTE 查询实现高效、单次、带层级的父子级分类检索——这比 PHP 内存递归或 withTree() 更适合深层级、高并发场景。
为什么优先考虑 CTE 而不是 withTree
withTree() 是查完所有数据再在 PHP 内存中构树,它不减少 SQL 查询次数,也不带层级深度、排序或分页能力;而 CTE 在数据库内完成递归,返回结果天然有序、可限制深度、可加索引优化,且不受 PHP 嵌套层数限制。
- 层级超 5 层时,
withTree()构树耗时明显上升,尤其当分类表有数百条记录 - CTE 只需一条 SQL,无 N+1 风险,也无需担心
id === pid导致的死循环 - 结果可直接附加
depth字段,方便前端控制缩进或折叠逻辑
MySQL 8.0+ CTE 实战写法(含自包含)
假设分类表为 categories,字段为 id、name、parent_id。以下 SQL 查出指定分类 ID(含自身)及其全部后代,并标记层级:
WITH RECURSIVE category_tree AS ( SELECT id, name, parent_id, 0 AS depth FROM categories WHERE id = ? UNION ALL SELECT c.id, c.name, c.parent_id, ct.depth + 1 FROM categories c INNER JOIN category_tree ct ON c.parent_id = ct.id ) SELECT id, name, parent_id, depth FROM category_tree ORDER BY depth, id;
- 参数
?替换为你想展开的父分类 ID(例如 1) - 索引建议:确保
parent_id字段有普通索引,否则 JOIN 性能骤降 - 若需排除自身,把基础查询中的
WHERE id = ?改为WHERE parent_id = ?
在 ThinkPHP 中安全调用 CTE
TP6 的 Db::query() 支持原生 SQL,但要注意参数绑定和语法校验:
- 不要拆分 CTE 语句,整个字符串传入
Db::query($sql, [$parentId]) - 避免拼接字符串,防止 SQL 注入;CTE 中的占位符
?会被 TP 正确绑定 - 执行前先验证数据库版本:
Db::query('SELECT VERSION()'),低于 8.0 则降级为全量查 + PHP 构树 - 返回结果是二维数组,可直接转 JSON 或传给模板,无需额外处理
children键
CTE 与 PHP 侧方案的协同策略
不必非此即彼。生产环境推荐组合使用:
- 菜单/类目首页:用 CTE 查一级 + 二级,前端点击再按需加载子级(URL 带
pid=xxx) - 后台编辑页:用 CTE 查完整子树(加
depth 防止意外过深) - 搜索或筛选场景:配合
path字段(如'-1-5-12-')做LIKE查询,比 CTE 更快 - 变更后主动清缓存:
Cache::delete('category_tree_' . $parentId),避免 CTE 结果陈旧
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











