[英]What are the options for storing hierarchical data in a relational database?
好的概述
一般來說,您是在快速讀取時間(例如,嵌套集)或快速寫入時間(鄰接表)之間做出決定。 通常,您最終會得到最適合您需求的以下選項的組合。 以下提供了一些深入的閱讀:
選項
我知道的和一般特征:
O(n/2)
移動、插入、刪除O(log n)
(子樹的大小)LEFT(lineage, #) = '/enumerated/path'
)O(log n)
(子樹的大小)數據庫特定說明
MySQL
甲骨文
PostgreSQL
SQL 服務器
我最喜歡的答案是這個線程中的第一句話所建議的。 使用鄰接列表來維護層次結構並使用嵌套集來查詢層次結構。
到目前為止的問題是從鄰接表到嵌套集的覆蓋方法非常緩慢,因為大多數人使用稱為“推送堆棧”的極端 RBAR 方法進行轉換,並且被認為是昂貴的通過鄰接表和嵌套集的出色性能達到維護簡單的涅槃。 結果,大多數人最終不得不滿足於一個或另一個,特別是如果有超過,比如說,糟糕的 100,000 個左右的節點。 使用推送堆棧方法可能需要一整天的時間來轉換傳銷者認為的百萬級節點層次結構。
我想我會給 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 萬個節點在不到 1 分鍾的時間內完成轉換,100,000 個節點在 4 秒內完成。
您可以閱讀有關新方法的信息並在以下 URL 獲取代碼副本。 http://www.sqlservercentral.com/articles/Hierarchy/94040/
我還使用類似的方法開發了一個“預聚合”層次結構。 傳銷者和制作物料清單的人會對本文特別感興趣。 http://www.sqlservercentral.com/articles/T-SQL/94570/
如果您確實停下來看看任何一篇文章,請跳轉到“加入討論”鏈接,讓我知道您的想法。
我選擇它是因為我可以輕松地將新項目插入到樹中(你只需要一個分支的 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
的項目,並按parent
排序。我需要比插入更快地訪問和查詢樹,這就是我選擇這個的原因
唯一的問題right
left
。 好吧,我為它創建了一個存儲過程,並在每次插入一個新項目時調用它,這在我的情況下很少見,但它真的很快。 我從 Joe Celko 的書中得到了這個想法,DBA SE https://dba.stackexchange.com/q/89051/41481中解釋了存儲過程以及我是如何想出它的
尚未提及此設計:
雖然它有局限性,但如果你能承受它們,它是非常簡單和非常有效的。 特征:
下面是一個示例 - 鳥類的分類樹,因此層次結構是 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 |
這很好,因為這樣您就可以非常輕松地完成所有需要的操作,只要內部類別不改變它們在樹中的級別。
這是對您問題的一個非常部分的答案,但我希望仍然有用。
Microsoft SQL Server 2008 實現了兩個對管理分層數據非常有用的功能:
查看 MSDN 上 Kent Tegels 撰寫的“使用 SQL Server 2008 建模您的數據層次結構”以了解開始。 另請參閱我自己的問題: SQL Server 2008 中的遞歸同表查詢
如果您的數據庫支持數組,您還可以將沿襲列或物化路徑實現為父 ID 數組。
特別是使用 Postgres,您可以使用集合運算符來查詢層次結構,並通過 GIN 索引獲得出色的性能。 這使得在單個查詢中查找父母、孩子和深度變得非常簡單。 更新也很容易管理。
如果你好奇的話,我有一個完整的關於使用數組作為物化路徑的文章。
這真的是一個方釘圓孔的問題。
如果關系數據庫和 SQL 是您擁有或願意使用的唯一錘子,那么到目前為止發布的答案就足夠了。 但是,為什么不使用旨在處理分層數據的工具呢? 圖數據庫是復雜層次數據的理想選擇。
與圖形數據庫解決方案可以輕松解決相同問題相比,關系模型的低效率以及將圖形/層次模型映射到關系模型的任何代碼/查詢解決方案的復雜性都是不值得的。
將物料清單視為一種常見的分層數據結構。
class Component extends Vertex {
long assetId;
long partNumber;
long material;
long amount;
};
class PartOf extends Edge {
};
class AdjacentTo extends Edge {
};
兩個子組件之間的最短路徑:簡單的圖遍歷算法。 可接受的路徑可以根據標准進行限定。
相似度:兩個程序集之間的相似度是多少? 對兩個子樹執行遍歷,計算兩個子樹的交集和並集。 相似百分比是交集除以並集。
傳遞閉包:遍歷子樹並總結感興趣的字段,例如“子組件中有多少鋁?”
是的,您可以使用 SQL 和關系數據庫來解決問題。 但是,如果您願意為工作使用正確的工具,還有更好的方法。
我正在為我的層次結構使用帶有閉包表的 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;
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.