What are the options for storing hierarchical data in a relational database?
良好的概览
一般来说,您要在快速读取时间(例如嵌套集)和快速写入时间(相邻列表)之间做出决定。通常情况下,您最终会得到以下最适合您需要的选项组合。下面提供了一些深入的阅读:
- 还有一个嵌套间隔与邻接列表比较:我发现的邻接列表、物化路径、嵌套集和嵌套间隔的最佳比较。
- 分层数据模型:对权衡和示例用法有很好解释的幻灯片
- 在MySQL中表示层次结构:特别是嵌套集的非常好的概述
- RDBMS中的层次数据:我见过的最全面和组织良好的一组链接,但在解释方面没有太多。
选项
我知道的和一般特征:
- 列:id,parentid
- 易于实施。
- 便宜的节点移动、插入和删除。
- 查找等级、祖先和后代、路径的成本很高
- 通过支持N+1的数据库中的公共表表达式避免N+1
- 列:左、右
- 廉价血统,后代
- 由于易失性编码,
O(n/2) 移动、插入和删除非常昂贵
- 使用单独的联接表:祖先、后代、深度(可选)
- 廉价的祖先和后代
- 为插入、更新、删除而写入成本
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 列。 - 如果您需要任何父级的所有后代,您可以查询在父级的
lft 和rgt 之间具有其lft 的项目。 - 如果需要任何节点的所有父节点到树的根节点,则查询
lft 小于节点的lft 和rgt 大于节点的rgt 的项目,并按parent 进行排序。
我需要使访问和查询树的速度比插入更快,这就是我选择这个的原因。
唯一的问题是在插入新项目时修复
如果您的数据库支持数组,那么您还可以将沿袭列或物化路径实现为父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; |