良好的概述

一般来说,您要在快速读取时间(例如,嵌套集)或快速写入时间(邻接列表)之间做出决定。通常情况下,您最终会得到以下选项的组合,这些选项最适合您的需求。以下内容提供了一些深入阅读:

还有一个嵌套间隔与相邻列表的比较:我找到的相邻列表、实体化路径、嵌套集和嵌套间隔的最佳比较。分层数据模型:幻灯片,对权衡和示例用法进行了很好的解释在MySQL中表示层次结构:特别是Nested Set的非常好的概述RDBMS中的分层数据:我见过的一组最全面、组织最严密的链接,但在解释方面不多

选项

我知道的和一般特征:

相邻列表:

列:ID,ParentID易于实施。廉价节点移动、插入和删除。查找级别、祖先和后代、路径的费用高昂通过支持N+1的数据库中的公共表表达式避免N+1

嵌套集(也称为修改的预排序树遍历)

列:左侧、右侧廉价的祖先、后代由于易失性编码,非常昂贵的O(n/2)移动、插入和删除

桥接表(也称为闭合表/带触发器)

使用具有祖先、后代和深度的单独联接表(可选)廉价的祖先和后代插入、更新和删除的写入成本O(log n)(子树的大小)规范化编码:适用于连接中的RDBMS统计和查询计划器每个节点需要多行

血统列(也称为物质化路径、路径枚举)

列:世系(例如/父/子/孙/等)通过前缀查询的廉价后代(例如LEFT(沿袭,#)='/enumerated/path')插入、更新和删除的写入成本O(log n)(子树的大小)非关系型:依赖于Array数据类型或序列化字符串格式

嵌套间隔

与嵌套集类似,但具有实数/浮点/十进制,因此编码不易失(移动/插入/删除)存在实数/浮点/小数表示法/精度问题矩阵编码变体为“自由”添加了祖先编码(物化路径),但增加了线性代数的复杂性。

平面工作台

修改后的相邻列表,为每个记录添加一个级别和等级(例如排序)列。迭代/分页成本低昂贵的移动和删除好用:线程讨论-论坛/博客评论

多个沿袭列

列:每个沿袭级别一个,指的是根级别的所有父级,从项级别向下的级别设置为NULL廉价的祖先、后代、水平廉价插入、删除、移动树叶昂贵的插入、删除和移动内部节点对层次结构深度的硬性限制

数据库特定说明

MySQL数据库

使用相邻列表的会话变量

神谕

使用CONNECT BY遍历相邻列表

PostgreSQL

物化路径的ltree数据类型

SQL Server

概述2008提供了HierarchyId数据类型,它似乎有助于使用LineageColumn方法并扩展可以表示的深度。


当前回答

我正在使用PostgreSQL和层次结构的闭包表。我有一个用于整个数据库的通用存储过程:

CREATE FUNCTION nomen_tree() RETURNS trigger
    LANGUAGE plpgsql
    AS $_$
DECLARE
  old_parent INTEGER;
  new_parent INTEGER;
  id_nom INTEGER;
  txt_name TEXT;
BEGIN
-- TG_ARGV[0] = name of table with entities with PARENT-CHILD relationships (TBL_ORIG)
-- TG_ARGV[1] = name of helper table with ANCESTOR, CHILD, DEPTH information (TBL_TREE)
-- TG_ARGV[2] = name of the field in TBL_ORIG which is used for the PARENT-CHILD relationship (FLD_PARENT)
    IF TG_OP = 'INSERT' THEN
    EXECUTE 'INSERT INTO ' || TG_ARGV[1] || ' (child_id,ancestor_id,depth) 
        SELECT $1.id,$1.id,0 UNION ALL
      SELECT $1.id,ancestor_id,depth+1 FROM ' || TG_ARGV[1] || ' WHERE child_id=$1.' || TG_ARGV[2] USING NEW;
    ELSE                                                           
    -- EXECUTE does not support conditional statements inside
    EXECUTE 'SELECT $1.' || TG_ARGV[2] || ',$2.' || TG_ARGV[2] INTO old_parent,new_parent USING OLD,NEW;
    IF COALESCE(old_parent,0) <> COALESCE(new_parent,0) THEN
      EXECUTE '
      -- prevent cycles in the tree
      UPDATE ' || TG_ARGV[0] || ' SET ' || TG_ARGV[2] || ' = $1.' || TG_ARGV[2]
        || ' WHERE id=$2.' || TG_ARGV[2] || ' AND EXISTS(SELECT 1 FROM '
        || TG_ARGV[1] || ' WHERE child_id=$2.' || TG_ARGV[2] || ' AND ancestor_id=$2.id);
      -- first remove edges between all old parents of node and its descendants
      DELETE FROM ' || TG_ARGV[1] || ' WHERE child_id IN
        (SELECT child_id FROM ' || TG_ARGV[1] || ' WHERE ancestor_id = $1.id)
        AND ancestor_id IN
        (SELECT ancestor_id FROM ' || TG_ARGV[1] || ' WHERE child_id = $1.id AND ancestor_id <> $1.id);
      -- then add edges for all new parents ...
      INSERT INTO ' || TG_ARGV[1] || ' (child_id,ancestor_id,depth) 
        SELECT child_id,ancestor_id,d_c+d_a FROM
        (SELECT child_id,depth AS d_c FROM ' || TG_ARGV[1] || ' WHERE ancestor_id=$2.id) AS child
        CROSS JOIN
        (SELECT ancestor_id,depth+1 AS d_a FROM ' || TG_ARGV[1] || ' WHERE child_id=$2.' 
        || TG_ARGV[2] || ') AS parent;' USING OLD, NEW;
    END IF;
  END IF;
  RETURN NULL;
END;
$_$;

然后,为每个具有层次结构的表创建一个触发器

CREATE TRIGGER nomenclature_tree_tr AFTER INSERT OR UPDATE ON nomenclature FOR EACH ROW EXECUTE PROCEDURE nomen_tree('my_db.nomenclature', 'my_db.nom_helper', 'parent_id');

对于从现有层次结构填充闭包表,我使用以下存储过程:

CREATE FUNCTION rebuild_tree(tbl_base text, tbl_closure text, fld_parent text) RETURNS void
    LANGUAGE plpgsql
    AS $$
BEGIN
    EXECUTE 'TRUNCATE ' || tbl_closure || ';
    INSERT INTO ' || tbl_closure || ' (child_id,ancestor_id,depth) 
        WITH RECURSIVE tree AS
      (
        SELECT id AS child_id,id AS ancestor_id,0 AS depth FROM ' || tbl_base || '
        UNION ALL 
        SELECT t.id,ancestor_id,depth+1 FROM ' || tbl_base || ' AS t
        JOIN tree ON child_id = ' || fld_parent || '
      )
      SELECT * FROM tree;';
END;
$$;

闭包表由3列定义:ANCESTOR_ID、DESCENTANT_ID、DEPTH。可以(我甚至建议)存储ANCESTOR和DESCENTANT值相同的记录,DEPTH值为零。这将简化检索层次结构的查询。它们确实很简单:

-- get all descendants
SELECT tbl_orig.*,depth FROM tbl_closure LEFT JOIN tbl_orig ON descendant_id = tbl_orig.id WHERE ancestor_id = XXX AND depth <> 0;
-- get only direct descendants
SELECT tbl_orig.* FROM tbl_closure LEFT JOIN tbl_orig ON descendant_id = tbl_orig.id WHERE ancestor_id = XXX AND depth = 1;
-- get all ancestors
SELECT tbl_orig.* FROM tbl_closure LEFT JOIN tbl_orig ON ancestor_id = tbl_orig.id WHERE descendant_id = XXX AND depth <> 0;
-- find the deepest level of children
SELECT MAX(depth) FROM tbl_closure WHERE ancestor_id = XXX;

其他回答

我最喜欢的答案是这篇文章的第一句话所暗示的。使用“相邻列表”维护层次结构,并使用“嵌套集”查询层次结构。

到目前为止的问题是,从邻接列表到嵌套集的转换方法非常缓慢,因为大多数人使用称为“推堆栈”的极端RBAR方法进行转换,并且被认为是达到邻接列表维护简单和嵌套集性能卓越的涅盘所需的昂贵方法。因此,大多数人最终不得不满足于其中一个或另一个,尤其是如果节点数量超过,比如说,糟糕的100000个左右。使用push stack方法可能需要一整天的时间来完成MLM认为是一个小的百万节点层次结构的转换。

我想,我可以想出一种方法,以看似不可能的速度将相邻列表转换为嵌套集,从而给Celko带来一些竞争。下面是我的i5笔记本电脑上push堆栈方法的性能。

Duration for     1,000 Nodes = 00:00:00:870 
Duration for    10,000 Nodes = 00:01:01:783 (70 times slower instead of just 10)
Duration for   100,000 Nodes = 00:49:59:730 (3,446 times slower instead of just 100) 
Duration for 1,000,000 Nodes = 'Didn't even try this'

这里是新方法的持续时间(括号中有push堆栈方法)。

Duration for     1,000 Nodes = 00:00:00:053 (compared to 00:00:00:870)
Duration for    10,000 Nodes = 00:00:00:323 (compared to 00:01:01:783)
Duration for   100,000 Nodes = 00:00:03:867 (compared to 00:49:59:730)
Duration for 1,000,000 Nodes = 00:00:54:283 (compared to something like 2 days!!!)

是的,没错。在不到一分钟内转换了100万个节点,在4秒内转换了10万个节点。

您可以阅读新方法并从以下URL获取代码副本。http://www.sqlservercentral.com/articles/Hierarchy/94040/

我还使用类似的方法开发了一个“预聚合”层次结构。传销商和制作材料清单的人对这篇文章特别感兴趣。http://www.sqlservercentral.com/articles/T-SQL/94570/

如果你真的停下来看看这两篇文章,请跳到“加入讨论”链接,让我知道你的想法。

如果数据库支持数组,还可以将沿袭列或物化路径实现为父ID的数组。

特别是在Postgres中,您可以使用set运算符来查询层次结构,并使用GIN索引获得优异的性能。这使得在单个查询中查找父项、子项和深度变得非常简单。更新也很容易管理。

如果你好奇的话,我有一篇关于将数组用于实体化路径的完整文章。

相邻模型+嵌套集模型

我之所以选择它,是因为我可以很容易地将新项目插入到树中(您只需要分支的id就可以向树中插入新项目),并且可以很快地查询它。

+-------------+----------------------+--------+-----+-----+
| category_id | name                 | parent | lft | rgt |
+-------------+----------------------+--------+-----+-----+
|           1 | ELECTRONICS          |   NULL |   1 |  20 |
|           2 | TELEVISIONS          |      1 |   2 |   9 |
|           3 | TUBE                 |      2 |   3 |   4 |
|           4 | LCD                  |      2 |   5 |   6 |
|           5 | PLASMA               |      2 |   7 |   8 |
|           6 | PORTABLE ELECTRONICS |      1 |  10 |  19 |
|           7 | MP3 PLAYERS          |      6 |  11 |  14 |
|           8 | FLASH                |      7 |  12 |  13 |
|           9 | CD PLAYERS           |      6 |  15 |  16 |
|          10 | 2 WAY RADIOS         |      6 |  17 |  18 |
+-------------+----------------------+--------+-----+-----+

每次需要任何父级的所有子级时,只需查询父列。如果需要任何父项的所有后代,请查询其lft介于父项的lft和rgt之间的项。如果需要任何节点到树根的所有父节点,可以查询lft低于节点lft且rgt大于节点rgt的项,并按父节点排序。

我需要使访问和查询树的速度比插入更快,这就是为什么我选择这个

唯一的问题是在插入新项目时修复左右列。我为它创建了一个存储过程,并在每次插入新项目时调用它,这在我的情况下很少见,但它确实很快。我从Joe Celko的书中得到了这个想法,存储过程以及我是如何想到它的,这在DBA SE中进行了解释https://dba.stackexchange.com/q/89051/41481

我正在使用PostgreSQL和层次结构的闭包表。我有一个用于整个数据库的通用存储过程:

CREATE FUNCTION nomen_tree() RETURNS trigger
    LANGUAGE plpgsql
    AS $_$
DECLARE
  old_parent INTEGER;
  new_parent INTEGER;
  id_nom INTEGER;
  txt_name TEXT;
BEGIN
-- TG_ARGV[0] = name of table with entities with PARENT-CHILD relationships (TBL_ORIG)
-- TG_ARGV[1] = name of helper table with ANCESTOR, CHILD, DEPTH information (TBL_TREE)
-- TG_ARGV[2] = name of the field in TBL_ORIG which is used for the PARENT-CHILD relationship (FLD_PARENT)
    IF TG_OP = 'INSERT' THEN
    EXECUTE 'INSERT INTO ' || TG_ARGV[1] || ' (child_id,ancestor_id,depth) 
        SELECT $1.id,$1.id,0 UNION ALL
      SELECT $1.id,ancestor_id,depth+1 FROM ' || TG_ARGV[1] || ' WHERE child_id=$1.' || TG_ARGV[2] USING NEW;
    ELSE                                                           
    -- EXECUTE does not support conditional statements inside
    EXECUTE 'SELECT $1.' || TG_ARGV[2] || ',$2.' || TG_ARGV[2] INTO old_parent,new_parent USING OLD,NEW;
    IF COALESCE(old_parent,0) <> COALESCE(new_parent,0) THEN
      EXECUTE '
      -- prevent cycles in the tree
      UPDATE ' || TG_ARGV[0] || ' SET ' || TG_ARGV[2] || ' = $1.' || TG_ARGV[2]
        || ' WHERE id=$2.' || TG_ARGV[2] || ' AND EXISTS(SELECT 1 FROM '
        || TG_ARGV[1] || ' WHERE child_id=$2.' || TG_ARGV[2] || ' AND ancestor_id=$2.id);
      -- first remove edges between all old parents of node and its descendants
      DELETE FROM ' || TG_ARGV[1] || ' WHERE child_id IN
        (SELECT child_id FROM ' || TG_ARGV[1] || ' WHERE ancestor_id = $1.id)
        AND ancestor_id IN
        (SELECT ancestor_id FROM ' || TG_ARGV[1] || ' WHERE child_id = $1.id AND ancestor_id <> $1.id);
      -- then add edges for all new parents ...
      INSERT INTO ' || TG_ARGV[1] || ' (child_id,ancestor_id,depth) 
        SELECT child_id,ancestor_id,d_c+d_a FROM
        (SELECT child_id,depth AS d_c FROM ' || TG_ARGV[1] || ' WHERE ancestor_id=$2.id) AS child
        CROSS JOIN
        (SELECT ancestor_id,depth+1 AS d_a FROM ' || TG_ARGV[1] || ' WHERE child_id=$2.' 
        || TG_ARGV[2] || ') AS parent;' USING OLD, NEW;
    END IF;
  END IF;
  RETURN NULL;
END;
$_$;

然后,为每个具有层次结构的表创建一个触发器

CREATE TRIGGER nomenclature_tree_tr AFTER INSERT OR UPDATE ON nomenclature FOR EACH ROW EXECUTE PROCEDURE nomen_tree('my_db.nomenclature', 'my_db.nom_helper', 'parent_id');

对于从现有层次结构填充闭包表,我使用以下存储过程:

CREATE FUNCTION rebuild_tree(tbl_base text, tbl_closure text, fld_parent text) RETURNS void
    LANGUAGE plpgsql
    AS $$
BEGIN
    EXECUTE 'TRUNCATE ' || tbl_closure || ';
    INSERT INTO ' || tbl_closure || ' (child_id,ancestor_id,depth) 
        WITH RECURSIVE tree AS
      (
        SELECT id AS child_id,id AS ancestor_id,0 AS depth FROM ' || tbl_base || '
        UNION ALL 
        SELECT t.id,ancestor_id,depth+1 FROM ' || tbl_base || ' AS t
        JOIN tree ON child_id = ' || fld_parent || '
      )
      SELECT * FROM tree;';
END;
$$;

闭包表由3列定义:ANCESTOR_ID、DESCENTANT_ID、DEPTH。可以(我甚至建议)存储ANCESTOR和DESCENTANT值相同的记录,DEPTH值为零。这将简化检索层次结构的查询。它们确实很简单:

-- get all descendants
SELECT tbl_orig.*,depth FROM tbl_closure LEFT JOIN tbl_orig ON descendant_id = tbl_orig.id WHERE ancestor_id = XXX AND depth <> 0;
-- get only direct descendants
SELECT tbl_orig.* FROM tbl_closure LEFT JOIN tbl_orig ON descendant_id = tbl_orig.id WHERE ancestor_id = XXX AND depth = 1;
-- get all ancestors
SELECT tbl_orig.* FROM tbl_closure LEFT JOIN tbl_orig ON ancestor_id = tbl_orig.id WHERE descendant_id = XXX AND depth <> 0;
-- find the deepest level of children
SELECT MAX(depth) FROM tbl_closure WHERE ancestor_id = XXX;

这是对你的问题的部分回答,但我希望仍然有用。

Microsoft SQL Server 2008实现了两个对管理分层数据非常有用的功能:

HierarchyId数据类型。公共表表达式,使用with关键字。

看看MSDN上Kent Tegels的“使用SQL Server 2008建立数据层次结构模型”。另请参阅我自己的问题:SQL Server 2008中的递归同表查询