Programmingデータエンジニア

SQLで階層的(ツリー状)データを効率的に処理および保存するにはどうすればよいですか?このための方法は何ですか、そしてタスクに適した方法をどのように選択しますか?

Hintsage AIアシスタントで面接を突破

回答。

階層構造を扱うことは、リレーショナルデータベースの古典です。例:ディレクトリ、ツリー状メニュー、組織構造。

問題の歴史: データベースはテーブルモデルです。ツリー状データには、各自の長所と短所を持ついくつかのアプローチがあります。

問題: 標準的なparent_idモデルは、遅い再帰的SELECTにつながります。実際のタスク(すべての子孫の検索、パス、サブツリーの数え上げ)は最適化を必要とします。

解決策:

  • 隣接リスト - parent_idへの単純な参照。
  • マテリアライズドパス - 完全なパスを反映する文字列。
  • ネストされたセット - 左/右の境界(left/right)を保存し、サブツリーを簡単に取り出すことができます。
  • クローズテーブル - ツリー内のすべての関係を反映する独自の関係テーブル(from->to)。

マテリアライズドパスのPostgreSQL用のコード例:

CREATE TABLE categories ( id SERIAL PRIMARY KEY, name TEXT, path TEXT ); -- パスの保存例: '1/2/5/'(ルート/サブカテゴリ/現在) SELECT * FROM categories WHERE path LIKE '1/2/%'; -- 2のすべての子孫

ネストされたセットのコード例:

CREATE TABLE nested_categories ( id SERIAL PRIMARY KEY, name TEXT, lft INT NOT NULL, rgt INT NOT NULL ); -- 子ノード SELECT * FROM nested_categories WHERE lft > 2 AND rgt < 15;

主な特徴:

  • 隣接リストは、単純なツリー状構造(低いネスト)に便利です。
  • マテリアライズドパスは、サブツリーの迅速な取得が可能で、実装が簡単です。
  • ネストされたセットは、すべての子孫を瞬時に取得することができ、読み取りが速いですが、変更が難しいです。

トリックのある質問。

すべての子孫を任意の深さで検索するために、単にparent_idを使用したネストされたSELECTを使うことはできますか?

これは浅い深さにのみ機能します。再帰的検索には、再帰的CTE(WITH RECURSIVE)またはより複雑なスキームが必要で、単純なJOINは膨大な数の呼び出しと悪いパフォーマンスにつながります。

例:

WITH RECURSIVE cte AS ( SELECT id, parent_id, name FROM categories WHERE id = 1 UNION ALL SELECT c.id, c.parent_id, c.name FROM categories c INNER JOIN cte ON c.parent_id = cte.id ) SELECT * FROM cte;

なぜ分子ツリー(ネストされたセット)はサブツリーを迅速に抽出しますが、ノードの追加/削除は遅いのですか?

ツリーを変更する際、lft/rgtを多くの行で同時に変更する必要があります(変更のステップは、挿入/削除よりも大きいすべての値です)。読み取りには理想的ですが、頻繁な変更には他の方法を使用してください。

parent_idやマテリアライズドパスではなく、クローズテーブルを使用するのはいつですか?

クローズテーブルは、任意のレベルの子孫への複数回のクエリと関係の定期的な再計算に完璧に機能します。欠点は、より多くのスペースを必要とすることです。

例:

CREATE TABLE closure ( ancestor INT, descendant INT, depth INT );

一般的な誤りとアンチパターン

  • 階層を単にparent_idを介して保存し、埋め込まれた構造の迅速な検索が必要な場合。
  • 補助関数なしでパスやlft/rgtを手動で再計算する。
  • 主要な列(parent_id/path/lft/rgt)にインデックスを欠如している。

実際の例

ネガティブケース

オンラインストアで、カテゴリはparent_idを介して実装されています。すべてのネストは手動で設定され、サブカテゴリの検索はネストされたSELECTを使用しています。

メリット:

  • シンプルで、拡張サポートは必要ありません。

デメリット:

  • ネストが3-4レベルになってもパフォーマンスが低下します。
  • ノードの移動操作は明確でなく論理エラーを引き起こします。

ポジティブケース

マテリアライズドパスまたはクローズテーブルを使用しています。すべてのサブカテゴリへのクエリは瞬時で、再計算はグループスクリプトで行います。

メリット:

  • スケーラビリティ。
  • 迅速なネスト選択。

デメリット:

  • 追加の計画が必要です。
  • 構造の変更時に負荷が増加します。