web-dev-qa-db-ja.com

ツリー構造用に最適化されたSQL

最高のパフォーマンスでデータベースからツリー構造のデータをどのように取得しますか?たとえば、データベースにフォルダ階層があるとします。 folder-database-rowに[〜#〜] id [〜#〜]NameおよびParentID列がある場合。

特別なアルゴリズムを使用してすべてのデータを一度に取得し、データベース呼び出しの量を最小限に抑えてコードで処理しますか?

または、データベースへの多くの呼び出しを使用して、データベースから直接構造を取得する方法を使用しますか?

たぶん、データベースの行のxの量、階層の深さ、または何に基づいて異なる答えがありますか?

編集:Microsoft SQL Serverを使用していますが、他の観点からの回答も興味深いです。

35
Seb Nilsson

これは、ツリーへのアクセス方法によって異なります。

賢い方法の1つは、すべてのノードに文字列IDを与えることです。親のIDは、子の予測可能な部分文字列です。たとえば、親は「01」、子は「0100」、「0101」、「0102」などになります。このようにして、次のようにしてデータベースからサブツリー全体を一度に選択できます。

SELECT * FROM treedata WHERE id LIKE '0101%';

基準は最初のサブストリングであるため、ID列のインデックスはクエリを高速化します。

16
Ned Batchelder

RDMSにツリーを格納するすべての方法のうち、最も一般的なのは隣接リストとネストされたセットです。ネストされたセットは読み取り用に最適化されており、単一のクエリでツリー全体を取得できます。隣接リストは書き込み用に最適化されており、簡単なクエリで追加できます。

隣接リストでは、各ノードには親ノードまたは子ノードを参照する列があります(他のリンクも可能です)。これを使用して、親子関係に基づいて階層を構築できます。残念ながら、ツリーの深さを制限しない限り、1つのクエリですべてをプルすることはできず、通常、ツリーの更新は更新より遅くなります。

入れ子になったセットモデルでは逆が真であり、読み取りは高速で簡単ですが、番号付けシステムを維持する必要があるため、更新は複雑になります。ネストされたセットモデルは、プレオーダーベースの番号付けシステムを使用してすべてのノードを列挙することにより、親子関係とソート順の両方をエンコードします。

私はネストされたセットモデルを使用しましたが、大きな階層を読み取り最適化するのは複雑ですが、それだけの価値があります。ツリーを描画してノードに番号を付ける演習をいくつか行うと、そのこつを理解できるはずです。

この方法に関する私の研究は、この記事から始まりました MySQLでの階層データの管理

15
Bernard Igiri

ネストされたセット 階層モデルを調べます。それはかなりクールで便利です。

13
Mladen Prajdic

私が取り組んでいる製品では、SQL Serverにいくつかのツリー構造が格納されており、上記の手法を使用してノードの階層をレコードに格納しています。つまり.

tblTreeNode
TreeID = 1
TreeNodeID = 100
ParentTreeNodeID = 99
Hierarchy = ".33.59.99.100."
[...] (actual data payload for node)

階層を維持することはもちろんトリッキーなビットであり、トリガーを利用します。ただし、親または子の階層には必要な情報がすべて含まれているため、挿入/削除/移動で生成することは再帰的ではありません。

したがって、ノードのすべての子孫を取得できます。

SELECT * FROM tblNode WHERE Hierarchy LIKE '%.100.%'

これが挿入トリガーです:

--Setup the top level if there is any
UPDATE T 
SET T.TreeNodeHierarchy = '.' + CONVERT(nvarchar(10), T.TreeNodeID) + '.'
FROM tblTreeNode AS T
    INNER JOIN inserted i ON T.TreeNodeID = i.TreeNodeID
WHERE (i.ParentTreeNodeID IS NULL) AND (i.TreeNodeHierarchy IS NULL)

WHILE EXISTS (SELECT * FROM tblTreeNode WHERE TreeNodeHierarchy IS NULL)
    BEGIN
        --Update those items that we have enough information to update - parent has text in Hierarchy
        UPDATE CHILD 
        SET CHILD.TreeNodeHierarchy = PARENT.TreeNodeHierarchy + CONVERT(nvarchar(10),CHILD.TreeNodeID) + '.'
        FROM tblTreeNode AS CHILD 
            INNER JOIN tblTreeNode AS PARENT ON CHILD.ParentTreeNodeID = PARENT.TreeNodeID
        WHERE (CHILD.TreeNodeHierarchy IS NULL) AND (PARENT.TreeNodeHierarchy IS NOT NULL)
    END

そしてここに更新トリガーがあります:

--Only want to do something if Parent IDs were changed
IF UPDATE(ParentTreeNodeID)
    BEGIN
        --Update the changed items to reflect their new parents
        UPDATE CHILD
        SET CHILD.TreeNodeHierarchy = CASE WHEN PARENT.TreeNodeID IS NULL THEN '.' + CONVERT(nvarchar,CHILD.TreeNodeID) + '.' ELSE PARENT.TreeNodeHierarchy + CONVERT(nvarchar, CHILD.TreeNodeID) + '.' END
        FROM tblTreeNode AS CHILD 
            INNER JOIN inserted AS I ON CHILD.TreeNodeID = I.TreeNodeID
            LEFT JOIN tblTreeNode AS PARENT ON CHILD.ParentTreeNodeID = PARENT.TreeNodeID

        --Now update any sub items of the changed rows if any exist
        IF EXISTS (
                SELECT * 
                FROM tblTreeNode 
                    INNER JOIN deleted ON tblTreeNode.ParentTreeNodeID = deleted.TreeNodeID
            )
            UPDATE CHILD 
            SET CHILD.TreeNodeHierarchy = NEWPARENT.TreeNodeHierarchy + RIGHT(CHILD.TreeNodeHierarchy, LEN(CHILD.TreeNodeHierarchy) - LEN(OLDPARENT.TreeNodeHierarchy))
            FROM tblTreeNode AS CHILD 
                INNER JOIN deleted AS OLDPARENT ON CHILD.TreeNodeHierarchy LIKE (OLDPARENT.TreeNodeHierarchy + '%')
                INNER JOIN tblTreeNode AS NEWPARENT ON OLDPARENT.TreeNodeID = NEWPARENT.TreeNodeID

    END

もう1ビット、ツリーノードでの循環参照を防ぐためのチェック制約:

ALTER TABLE [dbo].[tblTreeNode]  WITH NOCHECK ADD  CONSTRAINT [CK_tblTreeNode_TreeNodeHierarchy] CHECK  
((charindex(('.' + convert(nvarchar(10),[TreeNodeID]) + '.'),[TreeNodeHierarchy],(charindex(('.' + convert(nvarchar(10),[TreeNodeID]) + '.'),[TreeNodeHierarchy]) + 1)) = 0))

また、ツリーごとに複数のルートノード(null親)を防ぎ、関連するノードが異なるTreeIDに属さないようにトリガーをお勧めします(ただし、これらは上記より少し簡単です)。

特定のケースをチェックして、このソリューションが許容範囲内で実行されるかどうかを確認する必要があります。お役に立てれば!

7
James Orr
4
Gene T

階層に対する一般的な種類のクエリがいくつかあります。他のほとんどの種類のクエリは、これらのバリエーションです。

  1. 親から、すべての子供を見つけます。

    a。特定の深さまで。たとえば、私の直接の親を考えると、深さ1までのすべての子供が私の兄弟になります。

    b。ツリーの一番下に。

  2. 子供から、すべての親を見つけます。

    a。特定の深さまで。たとえば、私の直接の親は、深さ1の親です。

    b。無制限の深さまで。

(a)ケース(特定の深さ)はSQLの方が簡単です。 SQLでは、特殊なケース(深さ= 1)は簡単です。ゼロ以外の深さはより困難です。有限であるがゼロではない深さは、有限数の結合を介して行うことができます。 (上から下まで)深さが不明確な(b)ケースは本当に難しいです。

ツリーが[〜#〜] huge [〜#〜](数百万のノード)の場合、何をしようとしても、傷ついた世界にいます。

ツリーが100万ノード以下の場合は、すべてをメモリにフェッチしてそこで作業します。 OO世界では、人生ははるかに単純です。行がフェッチされ、行が返されたらツリーを構築するだけです。

Hugeツリーがある場合、2つの選択肢があります。

  • 無制限のフェッチを処理するための再帰的カーソル。これは、構造のメンテナンスがO(1)-いくつかのノードを更新するだけで完了したことを意味します。ただし、フェッチはO(n * log(n))でなければなりません。子を持つノードごとにカーソルを開きます。

  • 賢い「ヒープ番号付け」アルゴリズムは、各ノードの親子関係をエンコードできます。各ノードに適切な番号が付けられると、4つのタイプのクエリすべてに簡単なSQL SELECTを使用できます。ただし、ツリー構造を変更すると、ノードの番号を付け直す必要があるため、変更のコストは取得のコストに比べてかなり高くなります。

2
S.Lott

データベースに多くのツリーがあり、ツリー全体しか取り出せない場合は、データベースの各ノードのツリーID(またはルートノードID)と親ノードIDを保存し、すべてのノードを取得します。特定のツリーID、およびメモリ内のプロセス。

ただし、サブツリーを取得する場合は、特定の親ノードIDのサブツリーしか取得できないため、上記の方法を使用するには、各ノードのすべての親ノードを格納するか、またはに降りるときに複数のSQLクエリを実行する必要があります。ツリー(ツリー内にサイクルがないことを願っています)。ただし、SQLの再コンパイルを防ぐために、同じPrepared Statementを再利用できます(ノードが同じタイプであり、すべてが1つのテーブルに格納されていると仮定)。遅くなることはありません。クエリにデータベースの最適化が適用されている場合は、それが望ましいでしょう。調べるためにいくつかのテストを実行したい場合があります。

1つのツリーのみを保存している場合、質問はサブツリーのみのクエリの1つになり、2番目の回答が適用されます。

1
JeeBee

私は、parentIDに関連付けられたIDを保存する簡単な方法のファンです。

ID     ParentID
1      null
2      null
3      1
4      2
...    ...

メンテナンスが簡単で、非常にスケーラブルです。

1
Galwegian

「Materialized Path」または「Genetic Trees」のGoogle ...

1
Thomas Hansen

Oracleには、ツリーを取得するためのSELECT ... CONNECT BYステートメントがあります。

1
Dmitry Khalatov

すべての状況で機能するわけではありませんが、たとえばコメント構造が与えられた場合:

ID | ParentCommentID

一番上のコメントを表すTopCommentIDを保存することもできます。

ID | ParentCommentID | TopCommentID

ここで、TopCommentIDおよびParentCommentIDnullまたは0最上位のコメントの場合。子コメントの場合、ParentCommentIDはその上のコメントを指し、TopCommentIDは最上位の親を指します。

0
Tom Gullen

この記事 は、リネージを派生列として保存する方法だけでなく、いくつかの取得方法も示しているので興味深いです。系統は、結合をあまり行わずに階層を取得するショートカットメソッドを提供します。

0
Turnkey