优化PHP代码,从大型数据集构建树形结构


Optimize PHP code to build Tree structure out of large dataset

我有一个程序来填充最高级别节点或父节点,以便在每个子节点和孙子节点旁边填充。

我首先做了一个树结构,然后通过解析填充每个子/孙子节点旁边的最高级别节点或根节点。

但是当我在大数据集>20000行上运行程序时,我得到这个错误:

Fatal error:  Out of memory (allocated 1807745024) (tried to allocate 36 bytes) in C:'xampp'htdocs'test_project'index.php on line 20
下面是我的代码:
<?php
include('mysql_config.php');
set_time_limit(0);
ini_set('memory_limit', '2048M');
$r = mysql_query("SELECT Emp_ID AS id,fname AS name,Manager_ID AS parent_id FROM targets");
        $data = array();
        while($row = mysql_fetch_assoc($r)) {
         $data[] = $row;
         }    
$j = mysql_query("SELECT Emp_ID AS id,fname AS name,Manager_ID AS parent_id FROM targets where Type = 'Super Manager'");
        $parent_data = array();
        while($row = mysql_fetch_assoc($j)) {
         $parent_data[] = $row;
         }           
function buildtree($src_arr, $parent_id = 0, $tree = array())
{
    foreach($src_arr as $idx => $row)
    {
        if($row['parent_id'] == $parent_id)
        {
            foreach($row as $k => $v)
                $tree[$row['id']][$k] = $v;
            unset($src_arr[$idx]);
            $tree[$row['id']]['children'] = buildtree($src_arr, $row['id']);
        }
    }
    ksort($tree);
    return $tree;
}
function fetch_recursive($tree, $parent_id, $parentfound = false, $list = array())
{
    foreach($tree as $k => $v)
    {
        if($parentfound || $k == $parent_id)
        {
            $rowdata = array();
            foreach($v as $field => $value)
                if($field != 'children')
                    $rowdata[$field] = $value;
            $list[] = $rowdata;
            if($v['children'])
                $list = array_merge($list, fetch_recursive($v['children'], $parent_id, true));
        }
        elseif($v['children'])
            $list = array_merge($list, fetch_recursive($v['children'], $parent_id));
    }
    return $list;
}
foreach($parent_data as $value)
{ 
echo '<pre>';
$result_data = fetch_recursive(buildtree($data),(int)$value['id']);
print_r($result_data);
echo '</pre>';
  if(!empty($result_data)){
   foreach($result_data as $child_val){
     $su_id=(int)$value['id'];
     $name_man=(string)$value['name'];
     $dest_id=$child_val['id'];
       mysql_query("update targets set SM_ID ='$su_id',SM_Name='$name_man' where Emp_ID='$dest_id'") or die (mysql_error());
     }
    }
}
?>

我如何优化代码来解决这个错误。我用100行尝试了这个代码,它工作得很好。

原始问题陈述

我有以下数据在我的数据库:

Manager_ID Employee_ID
AAA   BBB
AAA   CCC
AAA   DDD
BBB   EEE
BBB   FFF
CCC   GGG
FFF   HHH
III   JJJ
JJJ   KKK
JJJ   LLL

我希望用它们各自的最高级根节点填充子节点,这样所有子节点都有一个根级数据/父映射到它们,就像这样:

Employee_ID 1st Level Node
AAA         Root
BBB         AAA
CCC         AAA
DDD         AAA
EEE         AAA
FFF         AAA
GGG         AAA
HHH         AAA
III         Root
JJJ         III
KKK         III
LLL         III

我已经尝试创建一个PHP函数来创建一个树,但我无法从那里把它填充到各自的子节点的最后或最高级根。

经验法则:PHP对每个标量值有40字节的开销。这些加起来很快。

不要使用已弃用的mysql_*接口;使用mysqli_*PDO

解决方案:将所有数据放入临时MySQL表中,然后使用SQL语句重新排列成分层格式。啊;你似乎已经把数据放在一个表中了。

SQL将涉及每个层次结构级别的SQL语句,可能在单个语句中处理该级别的所有记录。PHP代码将控制级别,因为MySQL没有层次结构的概念。

很有可能这个改变甚至会比PHP运行得更快——你最终得到的查询数量很少,而不是你目前设计的20000(或更多)个。

这是一种转换代码的方法。从最内层的for循环开始。考虑一下如何将该循环转换为一个 SQL语句。那就去做吧。(这一步之后你可能会很开心)

显然在内存中重新创建树不起作用。对于在关系数据库中描述树的问题,简单的自引用表确实是一个简单而明显的解决方案,但是正如您所发现的,它也非常有限。如果您的数据存储在邻接表中,这将相对容易。

如果我们有一个可以更新的字段,那么可以迭代地标记树,例如....

 run_query('update yourtable set flag=0');
 foreach ($root_id as $root) {
     $d=1;
     run_query('update yourtable set flag=$d where id=$root');
     do {
        $rows_changed=run_query('update yourtable y1 
           set y1.flag=$d+1
           where y1.parent in (select y2.id from yourtable y2
             where y2.flag=$d');
        ++$d;
     } while ($rows_changed);
     run_query('update yourtable set root=$root, flag=0
        where flag>0');
 }

(注代码未测试)。

当然,这可以在mysql过程中实现,它将比php/mysql混合解决方案运行得稍微快一些。

首先为每个子节点的根ID添加一列:

ALTER TABLE targets ADD COLUMN Root_ID INT;

然后将根ID设置为管理员的ID:

$sql = "UPDATE targets SET Root_ID = Manager_ID";
if (!$conn->query($sql)) {
    error_log($conn->error);
    die("Database error occurred, see log.");
}

然后迭代地将根ID更新为该员工的经理ID。当没有进行任何更改时循环停止,因为不存在根ID本身具有管理器的子节点。此时,所有记录的根ID将被设置为没有管理员的员工记录(即根员工)。

$affectedRows = 1;
while ($affectedRows > 0) {
    $sql = "UPDATE targets AS child
        INNER JOIN targets AS parent
          ON child.Root_ID = parent.Emp_ID
        SET child.Root_Id = parent.Manager_ID";
    if (!$conn->query($sql)) {
        error_log($conn->error);
        die("Database error occurred, see log.");
    }
    $affectedRows = $conn->affected_rows;
    echo "Changed $affectedRows rows'n"; // demo only, remove this echo
}

现在您可以逐行打印出员工及其根,而不必同时存储所有数据:

$sql = "(SELECT Emp_ID, Root_ID FROM targets)
    UNION
    (SELECT t1.Manager_ID, 'Root' FROM targets AS t1
     LEFT OUTER JOIN targets AS t2 ON t1.Manager_ID = t2.Emp_ID
     WHERE t2.Emp_ID IS NULL)
    ORDER BY Emp_ID";
if (($result = $conn->query($sql, MYSQLI_USE_RESULT)) === false) {
    error_log($conn->error);
    die("Database error occurred, see log.");
}
printf("%-10s %-10s'n", "Emp_ID", "Root_ID");
while ($row = $result->fetch_assoc()) {
    printf("%-10s %-10s'n", $row["Emp_ID"], $row["Root_ID"]);
}
$result->free();
输出:

Emp_ID     Root_ID   
AAA        Root      
BBB        AAA       
CCC        AAA       
DDD        AAA       
EEE        AAA       
FFF        AAA       
GGG        AAA       
HHH        AAA       
III        Root      
JJJ        III       
KKK        III       
LLL        III       

你的评论:

为了优化这一点,我添加了索引PRIMARY KEY (Emp_ID, Manager_ID)。下面是我的表定义:

CREATE TABLE `targets` (
  `Manager_Id` char(3) NOT NULL,
  `Emp_Id` char(3) NOT NULL,
  `Root_ID` char(3) DEFAULT NULL,
  PRIMARY KEY (`Emp_Id`,`Manager_Id`)
) ENGINE=InnoDB;

下面是UPDATE:

的查询EXPLAIN
*************************** 1. row ***************************
               id: 1
      select_type: UPDATE
            table: child
       partitions: NULL
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 10
         filtered: 100.00
            Extra: Using where
*************************** 2. row ***************************
               id: 1
      select_type: SIMPLE
            table: parent
       partitions: NULL
             type: ref
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 3
              ref: test.child.Root_ID
             rows: 1
         filtered: 100.00
            Extra: Using index

下面是SELECT UNION的EXPLAIN:

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: targets
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 10
     filtered: 100.00
        Extra: NULL
*************************** 2. row ***************************
           id: 2
  select_type: UNION
        table: t1
   partitions: NULL
         type: index
possible_keys: NULL
          key: PRIMARY
      key_len: 6
          ref: NULL
         rows: 10
     filtered: 100.00
        Extra: Using index
*************************** 3. row ***************************
           id: 2
  select_type: UNION
        table: t2
   partitions: NULL
         type: ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 3
          ref: test.t1.Manager_Id
         rows: 1
     filtered: 100.00
        Extra: Using where; Not exists; Using index
*************************** 4. row ***************************
           id: NULL
  select_type: UNION RESULT
        table: <union1,2>
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: NULL
     filtered: NULL
        Extra: Using temporary; Using filesort