关于sql:在关系数据库中存储分层数据的选项有哪些?

What are the options for storing hierarchical data in a relational database?

良好的概览

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

  • 还有一个嵌套间隔与邻接列表比较:我发现的邻接列表、物化路径、嵌套集和嵌套间隔的最佳比较。
  • 分层数据模型:对权衡和示例用法有很好解释的幻灯片
  • 在MySQL中表示层次结构:特别是嵌套集的非常好的概述
  • RDBMS中的层次数据:我见过的最全面和组织良好的一组链接,但在解释方面没有太多。

选项

我知道的和一般特征:

  • 邻接表:
    • 列:id,parentid
    • 易于实施。
    • 便宜的节点移动、插入和删除。
    • 查找等级、祖先和后代、路径的成本很高
    • 通过支持N+1的数据库中的公共表表达式避免N+1
  • 嵌套集(即修改的预排序树遍历)
    • 列:左、右
    • 廉价血统,后代
    • 由于易失性编码,O(n/2)移动、插入和删除非常昂贵
  • 桥接表(A.K.A.闭合表/W触发器)
    • 使用单独的联接表:祖先、后代、深度(可选)
    • 廉价的祖先和后代
    • 为插入、更新、删除而写入成本O(log n)(子树大小)
    • 规范化编码:适用于联接中的RDBMS统计和查询规划器
    • 每个节点需要多行
  • 沿袭列(也称为物化路径,路径枚举)
    • 列:世系(例如/父/子/孙/等)
    • 通过前缀查询的廉价后代(如LEFT(lineage, #) = '/enumerated/path')
    • 为插入、更新、删除而写入成本O(log n)(子树大小)
    • 非关系:依赖数组数据类型或序列化字符串格式
  • 嵌套间隔
    • 与嵌套集类似,但使用实/浮/小数,这样编码就不会不稳定(便宜的移动/插入/删除)
    • 存在实/浮/十进制表示/精度问题
    • 矩阵编码变体为"自由"添加了祖先编码(物化路径),但增加了线性代数的复杂性。
  • 平板桌
    • 一种修改过的邻接表,它向每条记录添加一个级别和等级(如排序)列。
    • 迭代/分页成本低
    • 昂贵的移动和删除
    • 很好的使用:线程式讨论-论坛/博客评论
  • 多个沿袭列
    • 列:每个沿袭级别一个,表示到根级别的所有父级,从项级别向下的级别设置为空。
    • 廉价的祖先、后代、等级
    • 便宜的插入、删除、移动树叶
    • 内部节点的插入、删除和移动代价高昂
    • 严格限制层次结构的深度
  • 数据库特定说明

    MySQL

    • 使用会话变量作为邻接列表

    甲骨文公司

    • 使用"连接方式"遍历相邻列表

    波斯特雷斯尔

    • 物化路径的ltree数据类型

    SQL Server

    • 概述
    • 2008年提供的hierarchyid数据类型似乎有助于沿袭列方法和扩展可表示的深度。


    我最喜欢的答案是这条线索的第一句话所建议的。使用邻接列表维护层次结构,并使用嵌套集查询层次结构。

    到目前为止的问题是,从邻接列表到嵌套集的覆盖方法速度非常缓慢,因为大多数人使用称为"推堆栈"的极端RBAR方法进行转换,并且被认为是达到邻接列表和aw维护简单性的涅盘的昂贵方法。esome嵌套集的性能。结果,大多数人最终只能选择其中一个或另一个节点,特别是当节点数超过100000个时。使用push stack方法可能需要一整天的时间来进行转换,而MLM认为这是一个小的百万节点层次结构。

    我想我会给塞尔科一点竞争,通过想出一种方法,以看起来不可能的速度将邻接列表转换为嵌套集。这是我的i5笔记本电脑上的推叠方法的性能。

    1
    2
    3
    4
    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'

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

    1
    2
    3
    4
    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/

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


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

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

    • HierarchyID数据类型。
    • 使用WITH关键字的公用表表达式。

    在msdn for starts上查看Kent Tegels的"使用SQL Server 2008对数据层次结构建模"。另请参见我自己的问题:SQL Server 2008中的递归相同表查询


    此设计尚未提及:

    多个沿袭列

    虽然它有局限性,但如果你能忍受,它是非常简单和高效的。特征:

    • 列:每个沿袭级别一个,表示到根目录为止的所有父级,低于当前项级别的级别设置为空。
    • 限制层次结构的深度
    • 廉价的祖先、后代、等级
    • 便宜的插入、删除、移动树叶
    • 内部节点的插入、删除和移动代价高昂

    下面是一个例子-鸟类的分类树,所以层次是类/目/科/属/种-物种是最低的层次,1行=1个分类单元(在叶节点的情况下对应于物种):

    1
    2
    3
    4
    5
    6
    7
    8
    CREATE TABLE `taxons` (
      `TaxonId` SMALLINT(6) NOT NULL DEFAULT '0',
      `ClassId` SMALLINT(6) DEFAULT NULL,
      `OrderId` SMALLINT(6) DEFAULT NULL,
      `FamilyId` SMALLINT(6) DEFAULT NULL,
      `GenusId` SMALLINT(6) DEFAULT NULL,
      `Name` VARCHAR(150) NOT NULL DEFAULT ''
    );

    数据示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    +---------+---------+---------+----------+---------+-------------------------------+
    | TaxonId | ClassId | OrderId | FamilyId | GenusId | Name                          |
    +---------+---------+---------+----------+---------+-------------------------------+
    |     254 |       0 |       0 |        0 |       0 | Aves                          |
    |     255 |     254 |       0 |        0 |       0 | Gaviiformes                   |
    |     256 |     254 |     255 |        0 |       0 | Gaviidae                      |
    |     257 |     254 |     255 |      256 |       0 | Gavia                         |
    |     258 |     254 |     255 |      256 |     257 | Gavia stellata                |
    |     259 |     254 |     255 |      256 |     257 | Gavia arctica                 |
    |     260 |     254 |     255 |      256 |     257 | Gavia immer                   |
    |     261 |     254 |     255 |      256 |     257 | Gavia adamsii                 |
    |     262 |     254 |       0 |        0 |       0 | Podicipediformes              |
    |     263 |     254 |     262 |        0 |       0 | Podicipedidae                 |
    |     264 |     254 |     262 |      263 |       0 | Tachybaptus                   |

    这是很好的,因为这样您就可以非常容易地完成所有需要的操作,只要内部类别不会改变它们在树中的级别。


    邻接模型+嵌套集模型

    我之所以这么做是因为我可以很容易地将新项目插入到树中(您只需要一个分支的ID就可以将新项目插入到树中),而且查询速度非常快。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    +-------------+----------------------+--------+-----+-----+
    | 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 |
    +-------------+----------------------+--------+-----+-----+
    • 每次需要任何父级的所有子级时,只需查询parent列。
    • 如果您需要任何父级的所有后代,您可以查询在父级的lftrgt之间具有其lft的项目。
    • 如果需要任何节点的所有父节点到树的根节点,则查询lft小于节点的lftrgt大于节点的rgt的项目,并按parent进行排序。

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

    唯一的问题是在插入新项目时修复leftright列。我为它创建了一个存储过程,每次我插入一个新项时都会调用它,这在我的例子中是罕见的,但它确实很快。我是从JoeCelko的书中得到这个想法的,存储过程以及我是如何想到它的在这里用DBASE解释的。https://dba.stackexchange.com/q/89051/41481


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

    特别是使用postgres,您可以使用set操作符来查询层次结构,并使用gin索引获得出色的性能。这使得在单个查询中查找父级、子级和深度非常简单。更新也相当容易管理。

    如果你好奇的话,我有一篇完整的关于使用数组作为物化路径的文章。


    这真是一个方钉,圆孔的问题。

    如果关系数据库和SQL是您唯一拥有或愿意使用的锤子,那么到目前为止发布的答案就足够了。但是,为什么不使用一个设计用来处理分层数据的工具呢?图形数据库是复杂层次数据的理想选择。

    关系模型的低效性以及将图形/层次模型映射到关系模型上的任何代码/查询解决方案的复杂性,与图形数据库解决方案解决相同问题的容易程度相比,根本不值得这么做。

    将物料清单视为常见的分层数据结构。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Component extends Vertex {
        long assetId;
        long partNumber;
        long material;
        long amount;
    };

    class PartOf extends Edge {
    };

    class AdjacentTo extends Edge {
    };

    两个子组件之间的最短路径:简单图遍历算法。可接受的路径可以根据标准进行限定。

    相似性:两个组件之间的相似程度是多少?对两个子树执行遍历,计算两个子树的交集和并集。相似的百分比是相交除以联合。

    传递闭包:遍历子树,总结感兴趣的领域,例如"子组件中有多少铝?"

    是的,您可以使用SQL和关系数据库来解决这个问题。但是,如果你愿意为工作使用合适的工具,那么有更好的方法。


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    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;
    $_$;

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

    1
    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');

    为了从现有层次结构填充关闭表,我使用以下存储过程:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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列定义——祖先_id、后代_id、深度。可以(我甚至建议)为祖先和后代存储具有相同值的记录,并且深度值为零。这将简化层次结构检索的查询。它们确实很简单:

    1
    2
    3
    4
    5
    6
    7
    8
    -- 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;