web-dev-qa-db-ja.com

リレーショナルデータベースに階層データを格納するためのオプションは何ですか?

概説

一般的に言って、あなたは速い読み込み時間(例えばネストされたセット)か速い書き込み時間(隣接リスト)の間で決定をしています。通常、あなたはあなたのニーズに最も合う以下のオプションの組み合わせで終わります。以下は、詳細な読み方を示しています。

オプション

私が知っているものと一般的な機能:

  1. 隣接リスト
    • 列:ID、ParentID
    • 実装が簡単.
    • 安いノードは移動、挿入、削除します。
    • レベル、祖先と子孫、パスを見つけるのに費用がかかる
    • Common Table Expressions を介してN + 1を回避する
  2. 入れ子集合 (別名 変更されたプレオーダツリートラバーサル
    • 列:左、右
    • 安い祖先、子孫
    • 非常に高価なO(n/2)はvolatileエンコーディングのために移動、挿入、削除します
  3. ブリッジテーブル (別名 クロージャテーブル/ wトリガー
    • 祖先、子孫、深さを指定して、別々の結合テーブルを使用します(オプション)
    • 安い祖先と子孫
    • 挿入、更新、削除のためのコストO(log n)サブツリーのサイズ)を書き込みます。
    • 正規化されたエンコーディング:結合時のRDBMS統計およびクエリプランナーに適しています
    • ノードごとに複数行が必要
  4. 系統列 (別名 実体化されたパス 、パスの列挙)
    • コラム:系統(例:/ parent/child/grandchild/etc ...)
    • 接頭辞クエリによる安い子孫(例:LEFT(lineage, #) = '/enumerated/path'
    • 挿入、更新、削除のためのコストO(log n)(サブツリーのサイズ)を書き込みます。
    • 非リレーショナル:Arrayデータ型またはシリアル化された文字列フォーマットに依存します
  5. 入れ子の間隔
    • 入れ子集合と似ていますが、エンコーディングが不安定にならないようにreal/float/decimalを使います(安価な移動/挿入/削除)。
    • 実数/浮動小数点/小数表現/精度の問題があります
    • 行列エンコーディングバリアント "free"の先祖エンコーディング(実体化されたパス)を追加しますが、線形代数のトリックを追加します。
  6. フラットテーブル
    • 各レコードに「レベル」と「ランク」(たとえば、順序付け)列を追加する、修正された隣接リスト。
    • 繰り返し/ページ付けが安い
    • 高価な移動と削除
    • 良い使い方:スレッドディスカッション - フォーラム/ブログのコメント
  7. 複数の系統列
    • 列:各系統レベルごとに1つ、ルートまでのすべての親を指し、アイテムのレベルから下のレベルはNULLに設定されます
    • 安い先祖、子孫、レベル
    • 安い挿入、削除、葉の移動
    • 高価な挿入、削除、内部ノードの移動
    • 階層の深さを制限する

_(データベース固有の注意事項

MySQL

Oracle

  • CONNECT BY を使用して隣接リストを走査します。

PostgreSQL

SQLサーバー

  • 概要
  • 2008オファー HierarchyId /データ型は、Lineage Columnアプローチを助け、表現できる深さを拡張するようです。
1202
orangepips

私のお気に入りの答えは、このスレッドの最初の文が提案したとおりです。隣接リストを使用して階層を維持し、ネストセットを使用して階層を照会します。

これまでの問題は、ほとんどの人が変換を行うために「プッシュスタック」として知られている極端なRBAR方法を使用し、高価になる方法であると考えられていたため隣接リストによるメンテナンスの単純さとNested Setsの素晴らしいパフォーマンスのNirvanaに到達するため。その結果、たいていの場合、10万ノード以上のノードがあるとしたら、ほとんどの人はどちらか一方に解決しなければならなくなります。プッシュスタック方式を使用すると、MLM層が数百万のノード階層であると見なすことになる変換に1日かかることがあります。

私は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分以内に、10万ノードが4秒以内に変換しました。

新しいメソッドについて読み、次のURLでコードのコピーを入手できます。 http://www.sqlservercentral.com/articles/Hierarchy/94040/

私はまた、同様の方法で「事前集計」階層を作成しました。 MLMおよび資料請求を作成する人々は、この記事に特に関心があります。 http://www.sqlservercentral.com/articles/T-SQL/94570/

どちらかの記事を見に行かない場合は、[ディスカッションに参加]リンクに移動して、あなたの考えを教えてください。

52
Jeff Moden

これはあなたの質問に対する非常に部分的な答えですが、それでも役に立つことを願っています。

Microsoft SQL Server 2008は、階層データの管理に非常に役立つ2つの機能を実装しています。

  • HierarchyId データ型。
  • with キーワードを使用した共通テーブル式。

「SQL Server 2008を使用したデータ階層のモデル化」 MSDNのKent Tegels著、まず始めにご覧ください。私自身の質問も参照してください。 SQL Server 2008の再帰的な同一テーブルクエリ

29
CesarGon

このデザインはまだ言及されていません:

複数の系統列

それには限界がありますが、あなたがそれらを負担することができれば、それは非常に単純で非常に効率的です。特徴:

  • 列:各系統レベルごとに1つ、ルートまでのすべての親を参照し、現在のアイテムのレベルより下のレベルは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                   |

内部カテゴリがツリー内のレベルを変更しない限り、この方法で必要なすべての操作を非常に簡単な方法で達成できるので、これは素晴らしいことです。

25
TMS

隣接モデル+入れ子セットモデル

簡単にツリーに新しいアイテムを挿入できるので(新しいアイテムを挿入するにはブランチの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列を照会するだけです。
  • 親の子孫がすべて必要な場合は、lftlftの間にあるrgtを持つアイテムをクエリします。
  • ツリーのルートまでのすべてのノードの親をすべて必要とする場合は、そのノードのlftよりも小さいlftおよびそのノードのrgtよりも大きいrgtを持つ項目を照会し、by parentでソートします。

挿入よりも早くツリーへのアクセスとクエリを実行する必要があるため、これを選択したのは

唯一の問題は、新しい項目を挿入するときにleft列とright列を修正することです。よく私はそれのためにストアドプロシージャを作成し、私が私の場合では稀だった新しいアイテムを挿入するたびにそれを呼び出しましたが、それは本当に速いです。私はJoe Celkoの本からアイデアを得ました、そしてストアード・プロシージャーと私がそれを思いついた方法はここでDBA SEで説明されます https://dba.stackexchange.com/q/89051/41481

17
azerafati

データベースが配列をサポートしている場合は、親列IDの配列として系統列またはマテリアライズドパスを実装することもできます。

特にPostgresでは、集合演算子を使って階層を問い合わせることができ、GINインデックスで優れたパフォーマンスを得ることができます。これにより、1回のクエリで両親、子供、奥行きを見つけることがかなり簡単になります。更新もかなり管理しやすいです。

実体化されたパスに 配列を使用することについての完全な記事を書いています あなたが興味を持っているならば。

13
Adam Sanderson

これは本当に四角い穴、丸穴の問題です。

リレーショナルデータベースとSQLがあなたが持っている、または使用したいと思っている唯一のハンマーであるならば、これまでに投稿された答えは十分です。しかし、階層データを処理するように設計されたツールを使用しないのはなぜでしょうか。 グラフデータベース 複雑な階層データに最適です。

グラフ/階層モデルをリレーショナルモデルにマッピングするためのコード/クエリソリューションの複雑さと、リレーショナルモデルの非効率性は、グラフデータベースソリューションが同じ問題を解決することができる容易さと比較すると、努力する価値がありません。

部品表を共通の階層データ構造として考えます。

class Component extends Vertex {
    long assetId;
    long partNumber;
    long material;
    long amount;
};

class PartOf extends Edge {
};

class AdjacentTo extends Edge {
};

2つのサブアセンブリ間の最短パス :単純なグラフ走査アルゴリズム。許容パスは、基準に基づいて修飾できます。

類似度 :2つのアセンブリ間の類似度は? 2つのサブツリーの交差と結合を計算する両方のサブツリーでトラバーサルを実行します。同様の割合は、交差点を和集合で割ったものです。

推移閉包 :サブツリーをたどり、関心のあるフィールドを合計します。 「サブアセンブリに含まれるアルミニウムの量は?」

はい、SQLとリレーショナルデータベースの問題を解決できます。しかし、あなたが仕事のために正しいツールを使用しても構わないと思っているなら、はるかに良いアプローチがあります。

9
djhallx

私は自分の階層のクロージャーテーブルでPostgreSQLを使用しています。データベース全体に対して1つのユニバーサルストアドプロシージャがあります。

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に0の値を持つレコードを格納することは可能です(そして私もお勧めします)。これにより、階層を取得するためのクエリが簡単になります。そしてそれらは非常に単純です。

-- 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;
5
IVO GELOV