概説
一般的に言って、あなたは速い読み込み時間(例えばネストされたセット)か速い書き込み時間(隣接リスト)の間で決定をしています。通常、あなたはあなたのニーズに最も合う以下のオプションの組み合わせで終わります。以下は、詳細な読み方を示しています。
オプション
私が知っているものと一般的な機能:
O(n/2)
はvolatileエンコーディングのために移動、挿入、削除しますO(log n)
サブツリーのサイズ)を書き込みます。LEFT(lineage, #) = '/enumerated/path'
)O(log n)
(サブツリーのサイズ)を書き込みます。_(データベース固有の注意事項
MySQL
Oracle
PostgreSQL
SQLサーバー
私のお気に入りの答えは、このスレッドの最初の文が提案したとおりです。隣接リストを使用して階層を維持し、ネストセットを使用して階層を照会します。
これまでの問題は、ほとんどの人が変換を行うために「プッシュスタック」として知られている極端な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/
どちらかの記事を見に行かない場合は、[ディスカッションに参加]リンクに移動して、あなたの考えを教えてください。
これはあなたの質問に対する非常に部分的な答えですが、それでも役に立つことを願っています。
Microsoft SQL Server 2008は、階層データの管理に非常に役立つ2つの機能を実装しています。
「SQL Server 2008を使用したデータ階層のモデル化」 MSDNのKent Tegels著、まず始めにご覧ください。私自身の質問も参照してください。 SQL Server 2008の再帰的な同一テーブルクエリ
このデザインはまだ言及されていません:
それには限界がありますが、あなたがそれらを負担することができれば、それは非常に単純で非常に効率的です。特徴:
次に例を示します - 階層が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 |
内部カテゴリがツリー内のレベルを変更しない限り、この方法で必要なすべての操作を非常に簡単な方法で達成できるので、これは素晴らしいことです。
簡単にツリーに新しいアイテムを挿入できるので(新しいアイテムを挿入するにはブランチの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
を持つ項目を照会し、by parent
でソートします。挿入よりも早くツリーへのアクセスとクエリを実行する必要があるため、これを選択したのは
唯一の問題は、新しい項目を挿入するときにleft
列とright
列を修正することです。よく私はそれのためにストアドプロシージャを作成し、私が私の場合では稀だった新しいアイテムを挿入するたびにそれを呼び出しましたが、それは本当に速いです。私はJoe Celkoの本からアイデアを得ました、そしてストアード・プロシージャーと私がそれを思いついた方法はここでDBA SEで説明されます https://dba.stackexchange.com/q/89051/41481
データベースが配列をサポートしている場合は、親列IDの配列として系統列またはマテリアライズドパスを実装することもできます。
特にPostgresでは、集合演算子を使って階層を問い合わせることができ、GINインデックスで優れたパフォーマンスを得ることができます。これにより、1回のクエリで両親、子供、奥行きを見つけることがかなり簡単になります。更新もかなり管理しやすいです。
実体化されたパスに 配列を使用することについての完全な記事を書いています あなたが興味を持っているならば。
これは本当に四角い穴、丸穴の問題です。
リレーショナルデータベースと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とリレーショナルデータベースの問題を解決できます。しかし、あなたが仕事のために正しいツールを使用しても構わないと思っているなら、はるかに良いアプローチがあります。
私は自分の階層のクロージャーテーブルで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;