以有效和简单的方式实现等级,父/子关系

时间:2012-06-16 15:50:09

标签: php mysql sql algorithm hierarchical-data

我有一张像

这样的表格
create table site
(
site_Id int(5),
parent_Id int(5),
site_desc varchar2(100)
);

领域的重要性:

  • site_Id:网站ID:
  • parent_Id:网站的父ID
  • site_desc:虽然与问题无关,但有网站说明

要求是如果我有一个site_id作为输入,我需要在网站下面标记所有标记。 例如:

                    A
                   / \
                  B   C
                / | \ /\
               D  E F G H
              /\
             I  J

所有节点都是site_Id。

该表包含以下数据:

Site_id  | Parent_ID  |  site_desc
_________|____________|___________
 A       |   -1       |   
 B       |    A       |
 C       |    A       |
 D       |    B       |
 E       |    B       |
 F       |    B       |
 I       |    D       |
 J       |    D       |

...

A是B和C的父亲,依此类推。

如果B是给定的输入,则查询需要获取D,E,I,F,J

目前通过循环中的多个查询实现,但我想在最少的查询数量中实现此目的。

我目前正在做的是::

向下投票

算法如下:

Initially create a data set object which you will populate, by fetching data from the data base. 
Create a method which takes the parent id as parameter and returns its child nodes if present, and returns -1, if it doesnt have a child. 
Step1: Fetch all the rows, which doesn't have a parent(root) node. 
Step2: Iterate through this result. For example if prod1 and prod2 are the initial returned nodes, in the resultset. 
Iterating this RS we get prod1, and we insert a row in our DataSET obj. 
Then we send the id of prod1 to getCHILD method, to get its child, and then again we iterate the returned resultset, and again call the getCHILD method, till we dont get the lowest node.

我需要在数据模型约束中使用最佳优化技术。如果您有任何建议,请随时回答 请提出建议。 提前谢谢。

10 个答案:

答案 0 :(得分:10)

不幸的是,如果您无法更改数据模型,并且您正在使用MySQL,那么您将陷入需要递归查询并且您正在使用不支持递归查询的DBMS的情况。 / p>

Quassnoi撰写了一系列有趣的博客文章,展示了查询分层数据的技巧。他的解决方案非常聪明,但非常复杂。 http://explainextended.com/2009/03/17/hierarchical-queries-in-mysql/

PostgreSQL是另一个开源RDBMS,它执行support recursive queries,因此您可以获取以您显示的方式存储的整个树。但是如果你不能改变数据模型,我假设你不能切换到不同的RDBMS。

有几种替代数据模型可以更容易地获取任意深度的树:

  • Closure Table
  • 嵌套集又名修改预订树遍历
  • 路径枚举又称物化路径

我在演示文稿Models for Hierarchical Data with SQL and PHP和我的书SQL Antipatterns: Avoiding the Pitfalls of Database Programming中介绍了这些内容。

最后,我在Slashdot的代码中看到了另一个解决方案,用于它们的注释层次结构:它们像在Adjacency List中一样存储“parent_id”,但它们也存储“root_id”列。给定树的每个成员对于root_id具有相同的值,root_id是其树中最高的祖先节点。然后在一个查询中很容易获取整个树:

SELECT * FROM site WHERE root_id = 123;

然后,您的应用程序将所有节点从数据库中取回到数组中,您必须编写代码以循环此数组,将节点插入内存中的树数据结构。如果您有许多单独的树,并且每个树的条目相对较少,这是一个很好的解决方案。对Slashdot来说这很好。

答案 1 :(得分:9)

昨天,我有answered这个question这与您描述的问题完全相关:在给定的邻接列表中,您希望得到特定的所有子节点parent - 也许是一个可以轻松迭代的一维数组。

您只需对数据库进行一次调用即可完成此操作,但有一点需要注意:您必须从表中返回 所有 行。 MySQL不支持递归查询,因此您必须在应用程序代码中执行SELECT

我只是重申我上面链接的答案,但基本上如果你以类似的格式返回结果集(可能来自PDOStatement->fetchAll(PDO::FETCH_ASSOC)或其他方法):

Array
(
    [0] => Array
    (
        [site_id] => A
        [parent_id] => -1
        [site_desc] => testtext
    )
    [1] => Array
    (
        [site_id] => B
        [parent_id] => A
        [site_desc] => testtext
    )
    [2] => Array
    (
        [site_id] => C
        [parent_id] => A
        [site_desc] => testtext
    )
    [3] => Array
    (
        [site_id] => D
        [parent_id] => B
        [site_desc] => testtext
    )
    [4] => Array
    (
        [site_id] => E
        [parent_id] => B
        [site_desc] => testtext
    )
    [5] => Array
    (
        [site_id] => F
        [parent_id] => B
        [site_desc] => testtext
    )
    [6] => Array
    (
        [site_id] => I
        [parent_id] => D
        [site_desc] => testtext
    )
    [7] => Array
    (
        [site_id] => J
        [parent_id] => D
        [site_desc] => testtext
    )
)

您可以使用此递归函数检索任何site_id的所有子/孙/伟大的孩子/等等(如果您知道id):

function fetch_recursive($src_arr, $id, $parentfound = false, $cats = array())
{
    foreach($src_arr as $row)
    {
        if((!$parentfound && $row['site_id'] == $id) || $row['parent_id'] == $id)
        {
            $rowdata = array();
            foreach($row as $k => $v)
                $rowdata[$k] = $v;
            $cats[] = $rowdata;
            if($row['parent_id'] == $id)
                $cats = array_merge($cats, fetch_recursive($src_arr, $row['site_id'], true));
        }
    }
    return $cats;
}

例如,假设您要检索site_id D的所有子项,您可以使用如下函数:

$nodelist = fetch_recursive($pdostmt->fetchAll(PDO::FETCH_ASSOC), 'D');
print_r($nodelist);

输出:

[0] => Array
(
    [site_id] => D
    [parent_id] => B
    [site_desc] => testtext
)
[1] => Array
(
    [site_id] => I
    [parent_id] => D
    [site_desc] => testtext
)
[2] => Array
(
    [site_id] => J
    [parent_id] => D
    [site_desc] => testtext
)

请注意,我们保留了父母及其子女,孙子女等的信息(无论嵌套深度如何)。

答案 2 :(得分:5)

如果您希望能够在单个查询中执行此操作,请查看嵌套集模型:http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/

另一种方法是在链接表中包含所有关系。因此,每个站点都会有一个指向其父级,祖父级等的链接。每个关系都是明确的。然后,您只需查询该链接表以获取所有后代。

答案 3 :(得分:3)

首先,我建议使用一种不同的存储树的方法: Closure Table 。如果您想了解更多信息,可以发现SQL Antipatterns书非常有趣。

那就是说。在我看来,生成这种结构的最简单方法是:http://jsbin.com/omexix/3/edit#javascript

我希望您在阅读JavaScript代码时没有问题。我使用它是因为在JavaScript中创建未分类的对象看起来并不那么黑客。可以通过使用多维数组来实现相同而不中继对象(或引用),但它看起来有点令人困惑。

这是算法的作用:

  • 我们遍历节点列表,一次
  • 如果节点的父节点未退出,则在数组中创建占位符
  • 如果节点没有父节点,则它位于根节点列表中
  • 如果节点在数组中没有占位符,则创建占位符
  • 来自节点的
  • 值分配给占位符
  • 节点已注册到父节点(如果它具有父节点

这是关于它的。基本上,您生成两个列表:包含所有节点,并且只有根节点。

答案 4 :(得分:3)

您可能想要查看闭包表模式。我发现这个site提供了丰富的信息。就我所见,还有几个关于这个概念的StackOverflow问题,例如here

答案 5 :(得分:2)

如果您不经常更新site表,可以使用以下策略:

create table site
(
site_Id int(5),
parent_Id int(5),
site_desc varchar2(100),
parents_path varchar(X)
);

parents_path等于从root选定节点的路径。例如,对于leaf J,它应该是|A|B|D|

优点: - 您需要单个查询才能获得结果;

缺点: - 更新期间的更多查询(但您可以明智地进行更新);

希望有所帮助

答案 6 :(得分:2)

其他人已经提出了如何通过对表格稍作修改来做到这一点 结构

如果您不想修改结构(即使这是最好的),那么您可以这样做 像这样:

  • SELECT * FROM site ORDER BY Parent_ID,Site_id;

通常可以安全地假设,一旦分配,ID不会改变;如果是ID 不要乱走,即节点C没有在节点B下移动,那么它就会被移动 确实,子节点的ID始终高于父节点和排序 以上将保证所有父母在孩子面前被抓住。

所以这些是假设:

- we prefer not to change the table layout
- we never change the IDs once assigned
- we never reorder the tree, moving IDs around

因此,可以在内存中创建树(甚至可以减少查询 本身添加WHERE Site_ID> = B)。

要通过的第一个节点将是B,并将被放入树中。

所有后续节点都可以存储在其Parent_ID节点中,这肯定是 之前加载。

这在Python中会很顺利(你可以直接修改父节点)。

请求“获取B的所有后代”可以在PHP中回答:

$nodes  = array( $parent_id );

$cursor = SQLQuery("SELECT * FROM site WHERE Site_ID > ? "
        .  "ORDER BY Parent_ID, Site_Id ;", $parent_id);

while ($tuple = SQLFetchTuple($cursor))
    if (in_array($tuple['Parent_ID'], $nodes))
        $nodes[] = $tuple['Site_Id'];
SQLFree($cursor);

// The first node is the global parent, and may be array_shift'ed away
    // if desired.

另一种方式
相当蛮力

另一种可能性是将“descendant_of”关系递归地存储在另一个中 表:

    TRUNCATE descendants;
    INSERT INTO descendants ( node, of ) VALUES ( -1, NULL );

    INSERT INTO descendants SELECT SiteId, ParentId FROM site JOIN
           descendants ON ( site.ParentId = descendants.of );

重复INSERT,直到插入的行数等于零(或总数) 后代中的行停止增加;在大多数数据库中查询表大小非常快。)

此时您将存储所有一级关系。现在:

INSERT IGNORE INTO descendants SELECT s1.node, s2.of FROM
           descendants AS s1 JOIN descendants AS s2 ON (s1.of = s2.node);

...再次直到后代停止增加(它将需要多个插入等于 最大级别数)。 JOIN总数将是级别数的两倍。

现在,如果您想获取节点16的所有后代,只需查询

即可
SELECT node FROM descendants WHERE of = 16;

答案 7 :(得分:2)

您可以为此创建存储过程。

这是我在mysql中的实现

DROP PROCEDURE IF EXISTS SearchTree;
DELIMITER go

CREATE PROCEDURE SearchTree( IN root CHAR(1) )
BEGIN
  DECLARE rows SMALLINT DEFAULT 0;
  DROP TABLE IF EXISTS reached;
  CREATE TABLE reached (
    site_Id CHAR(1) PRIMARY KEY
  ) ENGINE=HEAP;
  INSERT INTO reached VALUES (root);
  SET rows = ROW_COUNT();
  WHILE rows > 0 DO
    INSERT IGNORE INTO reached 
      SELECT DISTINCT s.site_Id 
      FROM site AS s 
      INNER JOIN reached AS r ON s.parent_Id = r.site_Id;
    SET rows = ROW_COUNT();
    DELETE FROM reached 
      WHERE site_Id = root;
  END WHILE;
  SELECT * FROM reached;
  DROP TABLE reached;
END;
go
DELIMITER ;
CALL SearchTree('B');

返回预期结果。

答案 8 :(得分:2)

根据您在此处的评论,我假设您不愿意更改现有数据模型,因为数百个应用程序正在使用它(如果您将其替换为其他应用程序,则会中断)。

问题的根源在于,对于任何站点,我们只知道它是直接父站点,因此我们需要递归查找该父站点的父站点,直到找到根站点。

如果您可以通过限制站点可以嵌套的深度/级别来限制,您可以编写一个很棒的查询来完成所有工作,甚至可能启动速度也很慢。触发查询的大部分开销来自设置连接,网络带宽等.MySQL可以非常快。

解雇多个查询会增加所有开销,因此我们不希望如此。 执行SELECT *然后在应用程序逻辑中进行计算意味着您每次都会获取所有数据,从而最大限度地增加网络开销,因此我们不希望这样。

如果树的深度限制是可接受的,您可以将多个查询组合成一个执行所有工作的大型查询,并返回所需的确切结果集。作为一个例子,我使用了你的数据,但用A,B,C等替换为1,2,3(因为你的列是int)。

要获取根节点的所有直接子节点(site_id = 1),请执行以下操作:

select site_id from site where parent_id = 1

要获取根节点的孙子,请执行以下操作:

select grandchild.site_id 
from site grandchild, site child 
where grandchild.parent_id = child.site_id 
and child.parent_id = 1

要获得根节点的曾孙,请执行以下操作:

select greatgrandchild.site_id 
from site greatgrandchild, site grandchild, site child 
where greatgrandchild.parent_id = grandchild.site_id 
and grandchild.parent_id = child.site_id 
and child.parent_id = 1

要获取根节点的所有后代,只需将上述查询合并到一个巨大的查询中,如下所示:

select site_id
from site
where site_id in (
    select site_id 
    from site 
    where parent_id = 1
)
or site_id in (
    select grandchild.site_id 
    from site grandchild, site child 
    where grandchild.parent_id = child.site_id 
    and child.parent_id = 1
)
or site_id in (
    select greatgrandchild.site_id 
    from site greatgrandchild, site grandchild, site child 
    where greatgrandchild.parent_id = grandchild.site_id 
    and grandchild.parent_id = child.site_id 
    and child.parent_id = 1
)

我想你知道这是如何运作的。对于每个额外级别,创建一个查询,查找与您正在搜索其后代的站点相距多个级别的节点,并将该查询添加到超级查询中,并在()中添加额外的'或site_id ...

现在你可以看到,只有三个级别,这已成为一个大问题。如果你需要支持10个级别,这个查询将会变得庞大,并且其中的所有OR和IN都会降低它...但是,它仍然可能比获取所有内容或使用多个查询更快。如果您需要支持任意数量的可能级别,则此查询无法帮助您。它必须变得无限大。在那种情况下,剩下的就是使用更好的方式...

那就是说,在你复制粘贴并开始编码之前,有一种方法可以避免这种巨大的查询,支持任意深度并且不会破坏向后兼容性。它确实需要更改数据模型,但它是一个小的,不会伤害使用此数据模型的其他程序。简而言之......

更好的方式

添加一个额外的列parent_paths,使用ravnur在他的回答中提到的编码从每个节点一直到根的完整路径

在插入,更新和删除时使用triggers动态填充该列。您现在正在维护冗余数据。它不会伤害其他程序,但可以为您的计划带来显着的性能优势。确保你的触发器是防弹的(这可能是最难的部分),因为额外列中的数据应始终与表中的常规数据同步

使用一个简短而甜蜜的查询,就像ravnur显示的那样,查找parent_paths列中任何位置的site_id的出现,直接获取该站点的所有后代,没有任何递归

答案 9 :(得分:1)

我还问自己如何递归地查询关系,我的大脑产生了这个解决方案(:

SELECT * FROM
(
    SELECT t2.* FROM table t1, table t2 where t2.parent = t1.id OR t2.parent 0 GROUP BY t2.id, t2.parent
) as all_relations
WHERE all_relations.parent >= '_the_id_'

# if you dont want a subtree use only the inner select

我不是百分百肯定,但我认为只要id自动递增并且子节点永远不会有较小的id作为其父节点(应该是正常情况),那么这可能是一个解决方案吗?