MySQL层次结构数据提取

时间:2010-11-12 22:13:55

标签: mysql hierarchical-data

我现在一次在一个查询上苦苦挣扎了大约2个小时。救命? :(

我有一张这样的表:

id    name                     lft      rgt        
35    Top level board          1        16     
37    2nd level board 3        6        15     
38    2nd level board 2        4        5     
39    2nd level board 1        2        3     
40    3rd level board 1        13       14     
41    3rd level board 2        9        12     
42    3rd level board 3        7        8     
43    4th level board 1        10       11

它存储在this tutorial中推荐的结构中。我想要做的是在所选论坛板下面选择一个论坛板和所有子论坛(不低)。理想情况下,查询将获得所选论坛的级别,同时只传递董事会的ID,然后它将选择该论坛,以及所有直接的孩子。

所以,我希望最终得到:

id    name                     lft      rgt        
35    Top level board          1        16  

37    2nd level board 3        6        15     
38    2nd level board 2        4        5     
39    2nd level board 1        2        3     

或者

id    name                     lft      rgt            
37    2nd level board 3        6        15  

40    3rd level board 1        13       14     
41    3rd level board 2        9        12     
42    3rd level board 3        7        8     

这里的顶行是父论坛,其他是子论坛。此外,我想要一个深度值给定的东西,深度是相对于选定的父窗体。例如,将最后一个表作为一些工作数据,我们将:

id    name                     lft      rgt      depth      
37    2nd level board 3        6        15       0

40    3rd level board 1        13       14       1
41    3rd level board 2        9        12       1
42    3rd level board 3        7        8        1

或者

id    name                     lft      rgt     depth      
35    Top level board          1        16      0

37    2nd level board 3        6        15      1
38    2nd level board 2        4        5       1
39    2nd level board 1        2        3       1

我希望你能在这里漂流。

任何人都可以帮忙吗?现在真的让我生气了:(

詹姆斯

3 个答案:

答案 0 :(得分:3)

最简单的方法 - 只需添加一个保持深度的列。 否则查询将是非常低效的 - 您将必须获得整个层次结构,按左数排序(将第一个孩子放在第一个),将其连接到自身以确保每个下一个节点左数等于上一节点右数+ 1

通常,嵌套区间算法很好,但是有一个严重的缺点 - 如果你向树中添加一些内容,需要进行大量的重新计算。 一个很好的替代方案是Tropashko嵌套区间算法与连续分数 - 只是谷歌为它。使用此算法获得低于父级的单个级别非常自然地完成。此外,给定一个孩子,您可以计算所有父母的所有数字而无需访问数据库。

答案 1 :(得分:0)

需要考虑的另一件事是关系数据库确实不是存储分层数据的最佳和最自然的方式。像你这样的结构 - 本质上是一个二叉树 - 可以更容易用XML blob表示,你可以持久存储,或者作为对象存储在面向对象的数据库中。

答案 2 :(得分:0)

我更喜欢自己的邻接列表方法。下面的示例使用非递归存储过程返回树/子树,然后我将其转换为XML DOM,但您可以使用结果集执行任何操作。请记住,这是从PHP到MySQL的单个调用,并且邻接列表更容易管理。

完整的脚本:http://pastie.org/1294143

PHP

<?php

header("Content-type: text/xml");

$conn = new mysqli("localhost", "foo_dbo", "pass", "foo_db", 3306);

// one non-recursive db call to get the tree

$result = $conn->query(sprintf("call department_hier(%d,%d)", 2,1));

$xml = new DomDocument;
$xpath = new DOMXpath($xml);

$dept = $xml->createElement("department");
$xml->appendChild($dept);

// loop and build the DOM

while($row = $result->fetch_assoc()){

    $staff = $xml->createElement("staff");
    // foreach($row as $col => $val) $staff->setAttribute($col, $val); 

    $staff->setAttribute("staff_id", $row["staff_id"]); 
    $staff->setAttribute("name", $row["name"]); 
    $staff->setAttribute("parent_staff_id", $row["parent_staff_id"]); 

    if(is_null($row["parent_staff_id"])){
        $dept->setAttribute("dept_id", $row["dept_id"]); 
        $dept->setAttribute("department_name", $row["department_name"]); 
        $dept->appendChild($staff);
    }
    else{
        $qry = sprintf("//*[@staff_id = '%d']", $row["parent_staff_id"]);
        $parent = $xpath->query($qry)->item(0);
        if(!is_null($parent)) $parent->appendChild($staff);
    }
}
$result->close();
$conn->close();

echo $xml->saveXML();
?>

XML输出

<department dept_id="2" department_name="Mathematics">
    <staff staff_id="1" name="f00" parent_staff_id="">
        <staff staff_id="5" name="gamma" parent_staff_id="1"/>
        <staff staff_id="6" name="delta" parent_staff_id="1">
            <staff staff_id="7" name="zeta" parent_staff_id="6">
                <staff staff_id="2" name="bar" parent_staff_id="7"/>
                <staff staff_id="8" name="theta" parent_staff_id="7"/>
            </staff>
        </staff>
    </staff>
</department>

SQL Stuff

-- TABLES

drop table if exists staff;
create table staff
(
staff_id smallint unsigned not null auto_increment primary key,
name varchar(255) not null
)
engine = innodb;

drop table if exists departments;
create table departments
(
dept_id tinyint unsigned not null auto_increment primary key,
name varchar(255) unique not null
)
engine = innodb;

drop table if exists department_staff;
create table department_staff
(
dept_id tinyint unsigned not null,
staff_id smallint unsigned not null,
parent_staff_id smallint unsigned null,
primary key (dept_id, staff_id),
key (staff_id),
key (parent_staff_id)
)
engine = innodb;

-- STORED PROCEDURES

drop procedure if exists department_hier;

delimiter #

create procedure department_hier
(
in p_dept_id tinyint unsigned,
in p_staff_id smallint unsigned
)
begin

declare v_done tinyint unsigned default 0;
declare v_dpth smallint unsigned default 0;

create temporary table hier(
 dept_id tinyint unsigned,
 parent_staff_id smallint unsigned, 
 staff_id smallint unsigned, 
 depth smallint unsigned
)engine = memory;

insert into hier select dept_id, parent_staff_id, staff_id, v_dpth from department_staff 
    where dept_id = p_dept_id and staff_id = p_staff_id;

/* http://dev.mysql.com/doc/refman/5.0/en/temporary-table-problems.html */

create temporary table tmp engine=memory select * from hier;

while not v_done do

    if exists( select 1 from department_staff e 
            inner join hier on e.dept_id = hier.dept_id and e.parent_staff_id = hier.staff_id and hier.depth = v_dpth) then

        insert into hier select e.dept_id, e.parent_staff_id, e.staff_id, v_dpth + 1 from department_staff e 
            inner join tmp on e.dept_id = tmp.dept_id and e.parent_staff_id = tmp.staff_id and tmp.depth = v_dpth;

        set v_dpth = v_dpth + 1;            

        truncate table tmp;
        insert into tmp select * from hier where depth = v_dpth;

    else
        set v_done = 1;
    end if;

end while;

select 
 hier.dept_id,
 d.name as department_name,
 s.staff_id,
 s.name,
 p.staff_id as parent_staff_id,
 p.name as parent_name,
 hier.depth
from 
 hier
inner join departments d on hier.dept_id = d.dept_id
inner join staff s on hier.staff_id = s.staff_id
left outer join staff p on hier.parent_staff_id = p.staff_id;

drop temporary table if exists hier;
drop temporary table if exists tmp;

end #

delimiter ;

-- TEST DATA

insert into staff (name) values 
    ('f00'),('bar'),('alpha'),('beta'),('gamma'),('delta'),('zeta'),('theta');

insert into departments (name) values
 ('Computing'),('Mathematics'),('English'),('Engineering'),('Law'),('Music');

insert into department_staff (dept_id, staff_id, parent_staff_id) values
(1,1,null), 
    (1,2,1), 
    (1,3,1), 
        (1,4,3),
            (1,7,4),
(2,1,null), 
    (2,5,1), 
    (2,6,1), 
        (2,7,6),
            (2,8,7),
            (2,2,7);

-- TESTING (call this sproc from your php)

call department_hier(1,1);

call department_hier(2,1);