良好概述
一般来说,您要在快速读取时间(例如,嵌套集)或快速写入时间(邻接列表)之间做出决定。通常,您最终会得到最适合您需求的以下选项组合。以下提供了一些深入阅读:
选项
我知道并且有一般特征:
O(n/2)
移动,插入,由于易失性编码而删除O(log n)
(子树的大小)LEFT(lineage, #) = '/enumerated/path'
)O(log n)
(子树的大小)数据库特定说明
的MySQL
甲骨文
的PostgreSQL
SQL Server
答案 0 :(得分:54)
我最喜欢的答案是这个帖子中第一句话的建议。使用邻接列表来维护层次结构并使用嵌套集来查询层次结构。
到目前为止的问题是从邻接列表到嵌套集的转换方法一直非常缓慢,因为大多数人使用称为“推送栈”的极端RBAR方法来进行转换,并且被认为是通过邻接列表和嵌套集的强大性能来达到Nirvana维护简单性的方法。结果,大多数人最终不得不满足于一个或另一个,特别是如果有超过10万个节点那么多。使用推送栈方法可能需要一整天的时间才能完成MLM'ers认为是一个小百万节点层次结构的转换。
我认为通过提出一种将邻接列表转换为嵌套集的方法,我认为我会给Celko带来一些竞争。这是我的i5笔记本电脑上推送堆栈方法的性能。
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'
这是新方法的持续时间(在括号中使用推送堆栈方法)。
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秒内转换了100,000个节点。
您可以阅读有关新方法的信息,并在以下网址获取代码的副本。 http://www.sqlservercentral.com/articles/Hierarchy/94040/
我还使用类似的方法开发了“预聚合”层次结构。 MLM'ers和制作材料清单的人将对本文特别感兴趣。 http://www.sqlservercentral.com/articles/T-SQL/94570/
如果你停下来看一下这两篇文章,请跳到“加入讨论”链接,让我知道你的想法。
答案 1 :(得分:29)
这是对你的问题的非常局部的答案,但我希望仍然有用。
Microsoft SQL Server 2008实现了两个对管理分层数据非常有用的功能:
在MSDN上查看Kent Tegels的"Model Your Data Hierarchies With SQL Server 2008"作为开始。另见我自己的问题:Recursive same-table query in SQL Server 2008
答案 2 :(得分:26)
此设计尚未提及:
虽然它有局限性,如果你能忍受它,它非常简单而且效率很高。特性:
下面是一个例子 - 鸟类的分类树,所以层次结构是Class / Order / Family / Genus / Species - 物种是最低级别,1行= 1个分类单元(对应于叶子节点的物种) :
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 ''
);
以及数据示例:
+---------+---------+---------+----------+---------+-------------------------------+
| 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 |
这很棒,因为只要内部类别不改变树中的级别,这种方式就可以非常简单的方式完成所有需要的操作。
答案 3 :(得分:17)
我之所以这样,是因为我可以轻松地将新项目插入树中(您只需要一个分支的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 |
+-------------+----------------------+--------+-----+-----+
parent
列。lft
和lft
之间rgt
的项目。lft
低于节点lft
且rgt
大于节点rgt
的项目1}}并按parent
排序。我需要比插入更快地访问和查询树,这就是我选择的原因
唯一的问题是在插入新项目时修复left
和right
列。好吧,我为它创建了一个存储过程,并在每次插入一个新项目时调用它,这在我的情况下是罕见的,但它确实很快。
我从Joe Celko的书中得到了这个想法,并且在DBA SE中解释了存储过程以及我如何提出它
https://dba.stackexchange.com/q/89051/41481
答案 4 :(得分:13)
如果您的数据库支持数组,您还可以将一个沿袭列或物化路径实现为父ID数组。
特别是对于Postgres,您可以使用set运算符查询层次结构,并使用GIN索引获得出色的性能。这使得在单个查询中查找父项,子项和深度非常简单。更新也很容易管理。
如果您感到好奇,我会完整地使用arrays for materialized paths。
答案 5 :(得分:9)
这实际上是一个方形钉,圆孔问题。
如果关系数据库和SQL是您拥有或愿意使用的唯一锤子,那么到目前为止已发布的答案就足够了。但是,为什么不使用专门用于处理分层数据的工具呢? Graph database是复杂分层数据的理想选择。
与图形数据库解决方案解决问题的难易程度相比,关系模型的低效率以及将图形/层次模型映射到关系模型的任何代码/查询解决方案的复杂性都是不值得的。同样的问题。
将物料清单视为通用的分层数据结构。
class Component extends Vertex {
long assetId;
long partNumber;
long material;
long amount;
};
class PartOf extends Edge {
};
class AdjacentTo extends Edge {
};
两个子装配之间的最短路径:简单的图形遍历算法。可接受的路径可以根据标准进行限定。
相似性:两个装配体之间的相似程度是多少?在两个子树上执行遍历,计算两个子树的交集和并集。相似的百分比是交叉点除以联合。
传递关闭:遍历子树并总结感兴趣的字段,例如“子组件中有多少铝?”
是的,您可以使用SQL和关系数据库解决问题。但是,如果您愿意使用正确的工具来完成工作,那么有更好的方法。
答案 6 :(得分:5)
我正在使用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,DESCENDANT_ID,DEPTH。有可能(我甚至建议)为ANCESTOR和DESCENDANT存储具有相同值的记录,为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;