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


1334

良い概要

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

オプション

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

  1. 隣接リスト
    • 列:ID、ParentID
    • 実装が簡単です。
    • 安価なノードが移動、挿入、削除されます。
    • レベル、祖先と子孫、パスを見つけるのに費用がかかる
    • それらをサポートするデータベースの共通テーブル式によるN + 1を回避する
  2. ネストされたセット(別名Modified Preorder Tree Traversal
    • 列:左、右
    • 安い祖先、子孫
    • O(n/2)揮発性エンコーディングのため、非常に高価な移動、挿入、削除
  3. ブリッジテーブル(別名クロージャテーブル/ wトリガー
    • 先祖、子孫、深さ(オプション)で個別の結合テーブルを使用します
    • 安い祖先と子孫
    • O(log n)挿入、更新、削除にかかるコスト(サブツリーのサイズ)
    • 正規化されたエンコーディング:RDBMS統計と結合のクエリプランナーに適しています
    • ノードごとに複数の行が必要
  4. 系統列(別名マテリアライズドパス、パス列挙)
    • 列:系統(例:/ parent / child / grandchild / etc ...)
    • 接頭辞クエリによる安い子孫(例LEFT(lineage, #) = '/enumerated/path'
    • O(log n)挿入、更新、削除にかかるコスト(サブツリーのサイズ)
    • 非リレーショナル:配列データ型またはシリアル化された文字列形式に依存
  5. ネストされた間隔
    • ネストされたセットのようですが、エンコードが揮発性にならないように実数/浮動小数点/ 10進数を使用します(安価な移動/挿入/削除)
    • 実数/浮動小数点/小数表現/精度の問題があります
    • マトリックスエンコーディングバリアントは、「無料」の祖先エンコーディング(マテリアライズドパス)を追加しますが、線形代数のトリッキーさが追加されます。
  6. フラットテーブル
    • 各レコードにレベルとランク(例:順序付け)列を追加する変更された隣接リスト。
    • 繰り返し/ページ分割が安い
    • 高価な移動と削除
    • 有効利用:スレッド化されたディスカッション-フォーラム/ブログのコメント
  7. 複数の系統列
    • 列:系統レベルごとに1つ、ルートまでのすべての親を参照し、アイテムのレベルから下のレベルはNULLに設定されます
    • 安い先祖、子孫、レベル
    • 安価な葉の挿入、削除、移動
    • 内部ノードの高価な挿入、削除、移動
    • 階層の深さのハード制限

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

MySQL

オラクル

  • CONNECT BYを使用して隣接リストをトラバースする

PostgreSQL

SQLサーバー

  • 総括
  • 2008はHierarchyIdデータ型を提供し、系統列アプローチに役立ち、表現できる深さを拡張するように見えます。

5
よるとslideshare.net/billkarwin/sql-antipatterns-strike-backページ77、Closure Tablesより優れているAdjacency ListPath EnumerationNested Sets使いやすさの面で(と私は同様の性能を推測しています)。
ギリ

ここには、非常に単純なバージョン、単純なBLOBがありません。階層に数十のアイテムしかない場合は、IDのシリアル化されたツリーが最適なオプションになる可能性があります。
Lothar

@Lothar:質問はコミュニティWikiなので、気軽に質問してください。その点についての私の考えは、XPATHなどの安定したクエリ言語を使用したXMLなどのある種のblob構造化をサポートするデータベースでのみ実行することです。それ以外の場合、SQLではなく、コードの取得、逆シリアル化、変更以外のクエリを実行する適切な方法がわかりません。そして、本当に多くの任意の要素が必要な問題がある場合は、私が使用して気に入っているNeo4JなどのNodeデータベースを使用する方がよいでしょう。
Orangepips


2
「概要」のMSDNリンクには、この記事は表示されなくなりました。これは、MSM Magazineの2008年9月版にありました。これは、CHMファイルとしてダウンロードするか、次のWebアーカイブから参照できます。web.archive.org/ web / 20080913041559
ケンプͩ

回答:


66

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

これまでの問題は、ほとんどの人が「プッシュスタック」と呼ばれる極端なRBARメソッドを使用して変換を行うため、隣接リストからネストされたセットへの変換方法が恐ろしく遅くなり、高価になる方法と考えられていました隣接リストによるメンテナンスのシンプルさとネストされたセットの素晴らしいパフォーマンスのニルヴァーナに到達します。その結果、ほとんどの人は、特に、たとえば、10万個程度の粗悪なノードが存在する場合は、どちらかで解決する必要があります。プッシュスタックメソッドを使用すると、MLMが小数の百万ノード階層と見なすものの変換を実行するのに1日かかることがあります。

隣接リストをネストされたセットに変換する方法を考え出すことで、不可能と思われる速度でセルコに競争を与えたいと思いました。これが私の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/

同様の方法を使用して、「事前集計」階層も開発しました。MLMの担当者と部品表を作成する人々は、この記事に特に関心があります。 http://www.sqlservercentral.com/articles/T-SQL/94570/

どちらかの記事をご覧になる場合は、[ディスカッションに参加]リンクにアクセスして、ご意見をお寄せください。


MLMerとは何ですか?
David Mann

MLM = "マルチレベルマーケティング"。アムウェイ、シャクリー、ACNなど
ジェフモーデン

31

これはあなたの質問に対する非常に部分的な回答ですが、私はまだ役に立てば幸いです。

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

見てい「データ階層をモデルとSQL Server 2008の」開始のためのMSDNのケントTegelsによると。私自身の質問も参照してください:SQL Server 2008の再帰同じテーブルクエリ


2
興味深いことに、HierarchyIdはそのことを知りませんでした:msdn.microsoft.com/en-us/library/bb677290.aspx
orangepips

1
確かに。私は多くの再帰的な階層データを扱っており、一般的なテーブル式は非常に便利です。概要については、msdn.microsoft.com / en-us / library / ms186243.aspxを参照してください
CesarGon、

28

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

複数の系統列

それには限界がありますが、あなたがそれらに耐えることができれば、それは非常にシンプルで非常に効率的です。特徴:

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

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


22

隣接モデル+入れ子集合モデル

新しい項目をツリーに簡単に挿入でき(新しい項目を挿入するにはブランチの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

挿入よりも高速にツリーにアクセスしてクエリを実行する必要があったため、これを選択しました

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


3
+1これは正当なアプローチです。私自身の経験から、重要なのは、大規模な更新操作が発生したときにダーティリードで問題ないかどうかを決定することです。そうでない場合は、問題になるか、テーブルに直接クエリを実行して、常にAPI(DB sprocs /関数またはコード)を実行できないようにします。
Orangepips

1
これは興味深い解決策です。ただし、親列にクエリを実行しても、子を見つけようとするときに大きな利点があるかどうかはわかりません。そのため、そもそも左列と右列があります。
トーマス

2
@Thomas、とには違いがchildrenありdescendantsます。leftそしてright子孫を見つけるために使用されています。
azerafati 2016年

14

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

特にPostgresでは、セット演算子を使用して階層をクエリし、GINインデックスで優れたパフォーマンスを得ることができます。これにより、1つのクエリで親、子、深度を簡単に見つけることができます。更新もかなり管理可能です。

興味があれば、具体化されたパスに配列を使用する方法について詳しく説明しています。


9

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

リレーショナルデータベースと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とリレーショナルデータベースで問題を解決できます。ただし、仕事に適したツールを使用したい場合は、より優れたアプローチがあります。


5
この回答は、RDBMSでSQLの代わりに、たとえばSPARQLを使用してグラフデータベースにクエリを実行する方法をユースケースで示した場合、または対照的に使用した場合、非常に役立ちます。
orangepips、2015年

1
SPARQLは、グラフデータベースのより大きなドメインのサブクラスであるRDFデータベースに関連しています。私はRDFデータベースではなく、現在SPARQLをサポートしていないInfiniteGraphを使用しています。InfiniteGraphは、いくつかの異なるクエリメカニズムをサポートしています。(1)ビュー、フィルター、パス修飾子、結果ハンドラーを設定するためのグラフナビゲーションAPI、(2)複雑なグラフパスパターンマッチング言語、(3)グレムリン。
djhallx 2015年

6

私は自分の階層のクロージャーテーブルで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の値がゼロのレコードを保存することもできます(私もアドバイスをしています)。これにより、階層を取得するためのクエリが簡略化されます。そして、それらは確かに非常に単純です:

-- 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;
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.