You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP非递归实现:将MySQL路径枚举树形表转为jqTree适配数组

问题描述

我在MySQL数据库中通过路径枚举结构创建了一张树形表,表结构如下:

+----+----------+-----------+-----------+
| id | name     | path      | id_parent |
+----+----------+-----------+-----------+
| 1  | node1    | /1/       | -1        |
| 2  | node2    | /1/2/     | 1         |
| 3  | node3    | /3/       | -1        |
| 4  | node4    | /3/4/     | 3         |
| 5  | node5    | /3/4/5/   | 4         |
+----+----------+-----------+-----------+

现在我希望将其转换为适配jqTree插件的PHP数组,格式如下:

$tree = [
    [
        'name' => 'node1',
        'id' => 1,
        'children' => [
            ['name' => 'node2', 'id' => 2]
        ]
    ],
    [
        'name' => 'node3',
        'id' => 3,
        'children' => [
            [
                'name' => 'node4',
                'id' => 4,
                'children' => [
                    ['name' => 'node5', 'id' => 5]
                ]
            ]
        ]
    ]
];

以下递归函数可以正常工作,但处理大数据时性能较差:

function createTreeDocs($idnode) {
    global $CON;
    $Q = "SELECT ID,NAME,PATH FROM documents where ID_PARENT={$idnode} order by PATH asc";
    $RES = $CON -> query( $Q );
    $NUM = $CON -> num( $RES );
    $tree = array();
    for ($i=0 ; $i<$NUM ; $i++) {
        $ROW = $CON -> fetch( $RES );
        $node = array("name"=> $ROW["NAME"], "id"=> $ROW["ID"]);
        $childs = createTreeDocs($ROW["ID"]);
        if (sizeof($childs)>0) $node["children"] = $childs;
        array_push($tree,$node);
    }
    return $tree;
}
print_r(createTreeDocs(-1));

请问如何在PHP中通过循环而非递归的方式实现该转换?


回答

好问题!递归处理树形结构虽然直观,但数据量大的时候确实会因为多次数据库查询和函数调用栈开销导致性能拉胯。我们可以通过一次性查询所有数据,再用循环构建树形结构的方式来优化,这样能大幅减少数据库交互次数,性能提升非常明显。

优化思路

  1. 一次性拉取所有数据:只执行一次SQL查询,避免递归时每个节点都触发一次数据库请求;
  2. 利用路径排序保证父节点优先:因为你的表用了路径枚举结构,按PATH升序排序后,父节点一定会出现在子节点之前,这样处理时能确保父节点已经被创建;
  3. 用数组映射存储节点引用:通过节点ID作为键,存储每个节点的引用,这样可以快速找到父节点并添加子节点,时间复杂度为O(n)。

具体实现代码

function createTreeDocsLoop() {
    global $CON;
    // 一次性查询所有节点,按PATH升序确保父节点先被处理
    $query = "SELECT ID, NAME, ID_PARENT FROM documents ORDER BY PATH ASC";
    $result = $CON->query($query);
    
    $nodeMap = []; // 用ID映射存储每个节点的引用,方便快速查找
    $tree = [];     // 最终的树形结构
    
    // 遍历所有节点,构建映射关系并组装树形结构
    while ($row = $CON->fetch($result)) {
        // 创建当前节点的基础结构
        $node = [
            'id' => $row['ID'],
            'name' => $row['NAME'],
            'children' => []
        ];
        
        // 存储节点引用到映射表
        $nodeMap[$row['ID']] = &$node;
        
        // 判断是否是根节点(父ID为-1)
        if ($row['ID_PARENT'] === -1) {
            $tree[] = &$node;
        } else {
            // 非根节点,找到父节点并将当前节点加入其子节点列表
            if (isset($nodeMap[$row['ID_PARENT']])) {
                $nodeMap[$row['ID_PARENT']]['children'][] = &$node;
            }
            // 可选:如果父节点不存在(数据异常),可以在这里加日志或处理逻辑
        }
    }
    
    // 释放引用,避免潜在的内存泄漏问题
    unset($nodeMap);
    
    return $tree;
}

// 使用示例
$tree = createTreeDocsLoop();
print_r($tree);

为什么这个方案性能更好?

  • 减少数据库查询次数:递归版本每个节点都要查一次数据库,数据量上千时就是上千次查询;这个版本只查一次,数据库交互成本直接降到最低;
  • 避免递归栈开销:递归深度过大时还可能触发PHP的栈溢出错误,循环版本完全没有这个问题;
  • 高效的节点查找:通过$nodeMap的ID映射,查找父节点是O(1)操作,整体构建过程是线性时间复杂度,处理大数据量时优势非常明显。

注意事项

  • 务必保证SQL查询按PATH升序排序,否则可能出现子节点先于父节点被处理,导致无法找到父节点的情况;
  • 使用引用(&)是为了避免复制整个节点数组,大幅降低内存开销;如果不用引用,每次添加子节点都会复制数组,数据量大时内存占用会飙升;
  • 如果你的数据库驱动返回的是对象而非数组,记得调整$row的访问方式(比如从$row['ID']改成$row->ID);
  • 可以根据需求添加错误处理,比如当父节点不存在时记录日志,避免数据异常导致的问题。

内容的提问来源于stack exchange,提问作者saleh mosleh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:25:06