簡體   English   中英

在關系數據庫中存儲分層數據的選項有哪些?

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

好的概述

一般來說,您是在快速讀取時間(例如,嵌套集)或快速寫入時間(鄰接表)之間做出決定。 通常,您最終會得到最適合您需求的以下選項的組合。 以下提供了一些深入的閱讀:

選項

我知道的和一般特征:

  1. 鄰接列表
  • 列:ID、ParentID
  • 易於實施。
  • 廉價的節點移動、插入和刪除。
  • 尋找關卡,祖先和后代,路徑的成本很高
  • 通過支持它們的數據庫中的公用表表達式避免 N+1
  1. 嵌套集(又名修改前序樹遍歷
  • 列:左,右
  • 廉價的祖先,后代
  • 由於易失編碼,非常昂貴的O(n/2)移動、插入、刪除
  1. 橋接表(又名閉包表 /w 觸發器
  • 使用帶有祖先、后代、深度的單獨連接表(可選)
  • 廉價的祖先和后代
  • 插入、更新、刪除的寫入成本為O(log n) (子樹的大小)
  • 規范化編碼:適用於連接中的 RDBMS 統計信息和查詢規划器
  • 每個節點需要多行
  1. 沿襲列(又名物化路徑,路徑枚舉)
  • 列:血統(例如 /parent/child/grandchild/etc...)
  • 通過前綴查詢的廉價后代(例如LEFT(lineage, #) = '/enumerated/path'
  • 插入、更新、刪除的寫入成本為O(log n) (子樹的大小)
  • 非關系:依賴 Array 數據類型或序列化字符串格式
  1. 嵌套區間
  • 像嵌套集,但使用實數/浮點數/小數,因此編碼不是易失性的(廉價的移動/插入/刪除)
  • 有實數/浮點數/十進制表示/精度問題
  • 矩陣編碼變體為“免費”添加了祖先編碼(物化路徑),但增加了線性代數的技巧。
  1. 平桌
  • 一個修改過的鄰接列表,為每條記錄添加一個級別和排名(例如排序)列。
  • 迭代/分頁便宜
  • 昂貴的移動和刪除
  • 良好用途:線程討論 - 論壇/博客評論
  1. 多個沿襲列
  • 列:每個血統級別一個,指的是所有父級到根,從項目的級別向下的級別設置為 NULL
  • 廉價的祖先,后代,等級
  • 廉價的插入、刪除、移動葉子
  • 內部節點的昂貴插入、刪除、移動
  • 層次結構深度的硬性限制

數據庫特定說明

MySQL

甲骨文

PostgreSQL

SQL 服務器

  • 一般總結
  • 2008 提供的HierarchyId數據類型似乎有助於使用 Lineage Column 方法並擴展可以表示的深度。

我最喜歡的答案是這個線程中的第一句話所建議的。 使用鄰接列表來維護層次結構並使用嵌套集來查詢層次結構。

到目前為止的問題是從鄰接表到嵌套集的覆蓋方法非常緩慢,因為大多數人使用稱為“推送堆棧”的極端 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在父項的lftrgt之間的項目。
  • 如果您需要直到樹根的任何節點的所有父節點,則查詢lft低於節點的lftrgt大於節點的rgt的項目,並按parent排序。

我需要比插入更快地訪問和查詢樹,這就是我選擇這個的原因

唯一的問題right left 好吧,我為它創建了一個存儲過程,並在每次插入一個新項目時調用它,這在我的情況下很少見,但它真的很快。 我從 Joe Celko 的書中得到了這個想法,DBA SE https://dba.stackexchange.com/q/89051/41481中解釋了存儲過程以及我是如何想出它的

尚未提及此設計:

多個沿襲列

雖然它有局限性,但如果你能承受它們,它是非常簡單和非常有效的。 特征:

  • 列:每個血統級別一個,指的是直到根的所有父項,當前項級別以下的級別設置為0(或NULL)
  • 層次結構的深度有一個固定限制
  • 廉價的祖先,后代,等級
  • 廉價的插入、刪除、移動葉子
  • 內部節點的昂貴插入、刪除、移動

下面是一個示例 - 鳥類的分類樹,因此層次結構是 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM