web-dev-qa-db-ja.com

SQLでデータツリーを表現する方法は?

TreeとTreeNodeを組み合わせたデータツリー構造を作成しています。ツリーには、データに対するルートアクションとトップレベルアクションが含まれます。 UIライブラリを使用して、ツリーをTreeViewにバインドできるウィンドウフォームでツリーを表示しています。

このツリーとノードをDBに保存する必要があります。ツリーを保存し、次の機能を取得する最良の方法は何ですか:

  1. 直感的な実装。
  2. 簡単なバインディング。ツリーからDB構造に移動し、(もしあれば)簡単に移動できます。

2つのアイデアがありました。 1つ目は、データをテーブル内の1つのライナーにシリアル化することです。 2番目はテーブルに保存することですが、データエンティティに移動すると、変更されたノード上のテーブルの行の状態が失われます。

何か案は?

40
Avi Harush

最も簡単な実装は、隣接リスト構造です:

id  parent_id  data

ただし、一部のデータベース、特にMySQLには、MySQLにない再帰クエリを実行する機能が必要なため、このモデルの処理に問題があります。

別のモデルは、入れ子集合です:

id lft rgt data

ここで、lftおよびrgtは、階層を定義する任意の値です(子のlftrgtは、親のlftrgt内にある必要があります)

これは再帰的なクエリを必要としませんが、保守が遅くなり難しくなります。

ただし、MySQLでは、SPATIAL abitiliesを使用してこれを改善できます。

私のブログでこれらの記事を参照してください。

より詳細な説明については。

33
Quassnoi

SQL-Antipatternsについてこのslidshareをブックマークしました。いくつかの代替案について説明しています。 http://www.slideshare.net/billkarwin/sql-antipatterns-strike-back?src=embed

そこからの推奨事項は、クロージャーテーブルを使用することです(スライドで説明されています)。

概要は次のとおりです(スライド77)。

                  | Query Child | Query Subtree | Modify Tree | Ref. Integrity
Adjacency List    |    Easy     |     Hard      |    Easy     |      Yes
Path Enumeration  |    Easy     |     Easy      |    Hard     |      No
Nested Sets       |    Hard     |     Easy      |    Hard     |      No
Closure Table     |    Easy     |     Easy      |    Easy     |      Yes
24
Björn

マテリアライズドパスソリューションについて言及している人がいないことに驚いています。これはおそらく、標準SQLでツリーを操作する最も速い方法です。

このアプローチでは、ツリー内のすべてのノードに列pathがあり、ルートからノードへのフルパスが格納されます。これには、非常にシンプルで高速なクエリが含まれます。

サンプルテーブルnodeをご覧ください:

+---------+-------+
| node_id | path  |
+---------+-------+
| 0       |       |
| 1       | 1     |
| 2       | 2     |
| 3       | 3     |
| 4       | 1.4   |
| 5       | 2.5   |
| 6       | 2.6   |
| 7       | 2.6.7 |
| 8       | 2.6.8 |
| 9       | 2.6.9 |
+---------+-------+

ノードxの子を取得するには、次のクエリを記述できます。

SELECT * FROM node WHERE path LIKE CONCAT((SELECT path FROM node WHERE node_id = x), '.%')

pathは、[〜#〜 ] like [〜#〜]句。

8
niutech

PostgreSQLを使用している場合、ltreeを使用できます。これは、ツリーデータ構造を実装するcontrib拡張機能のパッケージ(デフォルトで付属)です。

docs から:

CREATE TABLE test (path ltree);
INSERT INTO test VALUES ('Top');
INSERT INTO test VALUES ('Top.Science');
INSERT INTO test VALUES ('Top.Science.Astronomy');
INSERT INTO test VALUES ('Top.Science.Astronomy.Astrophysics');
INSERT INTO test VALUES ('Top.Science.Astronomy.Cosmology');
INSERT INTO test VALUES ('Top.Hobbies');
INSERT INTO test VALUES ('Top.Hobbies.Amateurs_Astronomy');
INSERT INTO test VALUES ('Top.Collections');
INSERT INTO test VALUES ('Top.Collections.Pictures');
INSERT INTO test VALUES ('Top.Collections.Pictures.Astronomy');
INSERT INTO test VALUES ('Top.Collections.Pictures.Astronomy.Stars');
INSERT INTO test VALUES ('Top.Collections.Pictures.Astronomy.Galaxies');
INSERT INTO test VALUES ('Top.Collections.Pictures.Astronomy.Astronauts');
CREATE INDEX path_Gist_idx ON test USING Gist (path);
CREATE INDEX path_idx ON test USING BTREE (path);

次のようなクエリを実行できます。

ltreetest=> SELECT path FROM test WHERE path <@ 'Top.Science';
                path
------------------------------------
 Top.Science
 Top.Science.Astronomy
 Top.Science.Astronomy.Astrophysics
 Top.Science.Astronomy.Cosmology
(4 rows)
5

データのクエリおよび更新方法によって異なります。すべてのデータを1つの行に格納する場合、基本的に単一のユニットであり、すべてのデータを書き換えずにクエリしたり、部分的に更新したりすることはできません。

各要素を行として保存する場合、最初に MySQLでの階層データの管理 (MySQL固有ですが、他の多くのデータベースにも当てはまります)を読む必要があります。

ツリー全体にのみアクセスする場合、隣接リストモデルにより、再帰クエリを使用せずにルートの下にあるすべてのノードを取得することが難しくなります。先頭にリンクする列を追加すると、SELECT * WHERE head_id = @idと1つの非再帰クエリでツリー全体を取得しますが、データベースを非正規化します。

一部のデータベースには、階層データの保存と取得を容易にするカスタム拡張機能があります。たとえば、Oracleには CONNECT BY があります。

3
Mark Byers

これはグーグル検索で「SQLツリー」を尋ねるときの一番の答えなので、今日(2018年12月)の観点からこれを更新しようとします。

ほとんどの答えは、隣接リストの使用が単純で遅いことを示唆しているため、他の方法を推奨します。

バージョン8(2018年4月に公開)以降、MySQLは 再帰共通テーブル式(CTE) をサポートします。 MySQLはショーに少し遅れていますが、これは新しいオプションを開きます。

チュートリアル here があり、再帰クエリを使用して隣接リストを管理する方法を説明しています。

再帰はデータベースエンジン内で完全に実行されるようになったため、過去(スクリプトエンジンで実行する必要があったとき)よりもはるかに高速になりました。

ブログ here は、いくつかの測定値(バイアスがあり、MySQLではなくpostgresの場合)を提供しますが、それでも、隣接リストを遅くする必要はないことを示しています。

したがって、今日の私の結論は次のとおりです。

  • データベースエンジンが再帰をサポートしている場合、単純な隣接リストは十分に高速です。
  • 独自のデータと独自のエンジンでベンチマークを実行します。
  • 「最良の」方法を指摘するための古い推奨事項を信用しないでください。
2
Holger Waldmann

最良の方法は、実際に各ノードにidとparent_idを与えることだと思います。ここで、親idは親ノードのidです。これにはいくつかの利点があります

  1. ノードを更新する場合は、そのノードのデータを書き換えるだけで済みます。
  2. 特定のノードのみを照会する場合、必要な情報を正確に取得できるため、データベース接続のオーバーヘッドが少なくなります。
  3. 多くのプログラミング言語には、mysqlデータをXMLまたはjsonに変換する機能があり、APIを使用してアプリケーションを簡単に開くことができます。
0
bigblind

各ノード行に(通常のノードデータに加えて)親IDが含まれるテーブル「ノード」のようなもの。ルートの場合、親はNULLです。

もちろん、これにより子供を見つけるのに少し時間がかかりますが、この方法では実際のデータベースは非常にシンプルになります。

0
Kimvais