2024年12月12日·1分で読めます

PostgreSQLでの組織図モデリング:隣接リスト vs クロージャー

隣接リストとクロージャーテーブルを比較し、フィルタ、レポート、権限チェックの明確な例とともにPostgreSQLで組織図をモデル化する方法を解説します。

PostgreSQLでの組織図モデリング:隣接リスト vs クロージャー

組織図がサポートすべきこと

組織図は誰が誰に報告しているか、チームがどのように部門に集約されるかを示す地図です。PostgreSQLで組織図をモデル化する際、単に各人に manager_id を保存するだけではありません。組織の閲覧、レポート、アクセスルールといった実業務をサポートする必要があります。

多くのユーザーは次の3つが瞬時に感じられることを期待します:組織の探索、人の検索、そして「自分の領域」に結果をフィルタリングすること。また、更新が安全であることも期待します。マネージャーが変わったとき、チャートはあらゆる場所で壊れることなく即座に更新されるべきです。

実務では、良いモデルは次のような繰り返し出てくる質問に答えられる必要があります:

  • この人の指揮系統(最上位まで)はどうなっているか?
  • このマネージャーの配下は誰か(直属の部下と全サブツリー)?
  • ダッシュボード用に人はどのようにチームや部門にグルーピングされるか?
  • 再編成はどのようにして不具合なく実行されるか?
  • 組織構造に基づき誰が何を見られるか?

組織は単純な木構造より難しくなります。チームは部門間を移動し、マネージャーはグループを入れ替え、ビューの一部は純粋に「人が人に報告する」だけではありません。例えば:人はチームに属し、チームは部門に属します。権限は別の層を加えます:組織の形が単なる図ではなくセキュリティモデルの一部になります。

いくつかの用語を整理しておきます:

  • ノード:階層内の1つの要素(人、チーム、部門)。
  • 親(parent):直上のノード(マネージャー、またはチームを所有する部門)。
  • 祖先(ancestor):任意の距離で上にあるノード(あなたの上司の上司など)。
  • 子孫(descendant):任意の距離で下にあるノード(あなたの配下全員)。

例:Salesが新しいVPの下に移った場合、2つのことが即座に正しくあるべきです。ダッシュボードは引き続き「Salesの全員」でフィルタできること、そして新しいVPの権限が自動的にSalesをカバーすること。

スキーマを選ぶ前に決めること

スキーマを決める前に、アプリが日々答えなければならないことを明確にしてください。「誰が誰に報告しているか?」は出発点にすぎません。多くの組織図では、部門のリーダーが誰か、誰がチームの休暇を承認するか、誰がレポートを見られるかも表示する必要があります。

画面や権限チェックが尋ねる正確な質問を書き出してください。質問を名前で挙げられないなら、見た目は正しくてもクエリしにくいスキーマになりがちです。

設計を決める主なポイント:

  • どのクエリを高速にする必要があるか:直属のマネージャー、CEOまでのチェーン、リーダー配下の全サブツリー、あるいは「この部門の全員」か?
  • 厳密なツリー(一人のマネージャー)か、マトリックス組織(複数のマネージャーやリード)か?
  • 部門は人と同じ階層に含めるか、それとも department_id のような属性として分けるか?
  • 誰かが複数のチームに属してよいか(共有サービスやスクワッド)?
  • 権限は木の下方に流れるか、上方か、または両方か?

これらの選択が「正しい」データの定義を決めます。例えば Alex が Support と Onboarding の両方を率いる場合、単一の manager_id や「チームにつき一人のリード」ルールは機能しないかもしれません。リーダーとチームの結合テーブルが必要になるか、「主チーム1つ+破線チーム」という方針をはっきりさせる必要があります。

部門もまた分岐点です。部門をノードにすると「Department A が Team B を含み Team B が Person C を含む」と表現できます。部門を別扱いにすると department_id = X でフィルタできて簡単ですが、チームが部門をまたぐと破綻する可能性があります。

最後に、権限を平易な言葉で定義してください。「マネージャーは自分の配下全員の給与を閲覧できるが、同僚は不可」は下方向ルールです。「誰でも自分の管理チェーンを見られる」は上方向ルールです。これを早期に決めると、どの階層モデルが自然に感じられるか、どれが後で高コストなクエリを強いるかが分かります。

隣接リスト:マネージャーとチームのためのシンプルなスキーマ

要素を最小限にしたいなら、隣接リストが古典的な出発点です。各人が直属のマネージャーへのポインタを保存し、これらのポインタを辿って木を作ります。

最小構成は次のようになります。

create table departments (
  id bigserial primary key,
  name text not null unique
);

create table teams (
  id bigserial primary key,
  department_id bigint not null references departments(id),
  name text not null,
  unique (department_id, name)
);

create table employees (
  id bigserial primary key,
  full_name text not null,
  team_id bigint references teams(id),
  manager_id bigint references employees(id)
);

別テーブルを省略して department_nameteam_nameemployees に持たせることもできます。開始は速いですが、タイプミスやチーム名変更、一貫性の欠如で管理が難しくなりがちです。別テーブルにするとフィルタや権限ルールを一貫して表現しやすくなります。

早めにガードレールを設けてください。階層データの修復は後で非常に辛いです。最低限、自己管理(manager_id \u003c\u003e id)は防いでください。また、マネージャーが同じチームや部門外でもよいか、ソフトデリートや履歴管理が必要かも決めておくべきです(監査用の報告ライン履歴など)。

隣接リストでは、ほとんどの更新が単純な書き込みです:マネージャー変更は employees.manager_id を更新し、チーム移動は employees.team_id を更新します(多くはマネージャーも同時に更新されます)。ただし小さな書き込みが大きな下流の影響を及ぼす点に注意してください。集計のロールアップが変わり、「マネージャーは自分の配下の全員を見られる」というルールは新しいチェーンに従う必要があります。

このシンプルさが隣接リストの最大の強みです。弱点は「このマネージャー配下の全員」を頻繁にフィルタするときに現れます。通常は都度再帰クエリで木を辿る必要があるためです。

隣接リスト:フィルタとレポートで使う一般的なクエリ

隣接リストでは、多くの有用な組織図の問いが再帰クエリになります。PostgreSQLでこの方法を使うと、以下のパターンを頻繁に使うことになります。

直属の部下(一段下)

マネージャーの即時チームは最も単純です。

SELECT id, full_name, title
FROM employees
WHERE manager_id = $1
ORDER BY full_name;

これは高速で読みやすいですが、一段下にしか行けません。

指揮系統(上方向)

ある人が誰に報告しているか(マネージャー、マネージャーのマネージャー……)を示すには再帰CTEを使います。

WITH RECURSIVE chain AS (
  SELECT id, full_name, manager_id, 0 AS depth
  FROM employees
  WHERE id = $1

  UNION ALL

  SELECT e.id, e.full_name, e.manager_id, c.depth + 1
  FROM employees e
  JOIN chain c ON e.id = c.manager_id
)
SELECT *
FROM chain
ORDER BY depth;

これは承認、エスカレーション、パンくずリストに役立ちます。

全サブツリー(下方向)

リーダー配下の全員を取得するには再帰の向きを逆にします。

WITH RECURSIVE subtree AS (
  SELECT id, full_name, manager_id, department_id, 0 AS depth
  FROM employees
  WHERE id = $1

  UNION ALL

  SELECT e.id, e.full_name, e.manager_id, e.department_id, s.depth + 1
  FROM employees e
  JOIN subtree s ON e.manager_id = s.id
)
SELECT *
FROM subtree
ORDER BY depth, full_name;

よくあるレポートは「リーダーYの下にいる、部門Xの全員」です:

WITH RECURSIVE subtree AS (
  SELECT id, department_id
  FROM employees
  WHERE id = $1
  UNION ALL
  SELECT e.id, e.department_id
  FROM employees e
  JOIN subtree s ON e.manager_id = s.id
)
SELECT e.*
FROM employees e
JOIN subtree s ON s.id = e.id
WHERE e.department_id = $2;

隣接リストのクエリは権限周りでリスクをはらみます。アクセスチェックがフルパス(閲覧者がその人の祖先か)に依存することが多いためです。もしエンドポイントのどれかが再帰を忘れたり、フィルタを間違った場所に適用すると行が漏洩します。またサイクルや欠落したマネージャーのようなデータ問題にも注意してください。一件の悪いレコードが再帰を壊したり予期せぬ結果を返したりします。したがって権限クエリには対策と堅牢な制約が必要です。

クロージャーテーブル:階層全体を保存する方法

生成されたコードを所有する
完全な制御が必要なときに実際のソースコードをエクスポートするオプションを保持します。
コードをエクスポート

クロージャーテーブルは直属リンクだけでなく、すべての祖先–子孫関係を保存します。木を一段ずつ辿る代わりに、「このリーダーの配下は誰か?」を単一の結合で得られます。

通常はノード(人やチーム)用のテーブルと階層パス用のテーブルの2つを保持します。

-- nodes
employees (
  id bigserial primary key,
  name text not null,
  manager_id bigint null references employees(id)
)

-- closure
employee_closure (
  ancestor_id bigint not null references employees(id),
  descendant_id bigint not null references employees(id),
  depth int not null,
  primary key (ancestor_id, descendant_id)
)

クロージャーテーブルは (Alice, Bob) のようなペアを格納します。これは「Alice は Bob の祖先である」という意味です。また ancestor_id = descendant_iddepth = 0 の自己行も保存します。この自己行は最初は奇妙に見えますが、多くのクエリを簡潔にします。

depth は二つのノード間の距離を示します:depth = 1 は直属のマネージャー、depth = 2 は上位のマネージャー、など。直属と間接の関係を区別する必要がある場合に重要です。

主な利点は読み取りの予測可能で高速な点です:

  • サブツリー全体の検索が高速(ディレクター配下の全員など)。
  • 指揮系統の取得が簡単(ある人の上位全員)。
  • depth を使えば直属と間接を分けられる。

代償は更新時の手間です。Bob が Alice から Dana にマネージャーを切り替えたとき、Bob と Bob 配下の全員のクロージャー行を再構築する必要があります。典型的な方法は、古い祖先パスを削除し、Dana の祖先と Bob のサブツリーの各ノードを組み合わせて新しいパスを挿入することです。

クロージャーテーブル:高速フィルタのための一般的なクエリ

両方の階層モデルをプロトタイプする
まず隣接リストを試し、より高速な読み取りが必要になったらクロージャーを追加します。
プロトタイプ作成

クロージャーテーブルは事前にすべての祖先–子孫ペアを保存するため(多くの場合 org_closure(ancestor_id, descendant_id, depth) の形)、組織フィルタが高速になります。ほとんどの問いが単一の結合に落ちます。

マネージャー配下の全員を列挙するには一度結合して depth で絞ります:

-- Descendants (everyone in the subtree)
SELECT e.*
FROM employees e
JOIN org_closure c
  ON c.descendant_id = e.id
WHERE c.ancestor_id = :manager_id
  AND c.depth > 0;

-- Direct reports only
SELECT e.*
FROM employees e
JOIN org_closure c
  ON c.descendant_id = e.id
WHERE c.ancestor_id = :manager_id
  AND c.depth = 1;

指揮系統(ある従業員の全祖先)を取得するには結合を逆にします:

SELECT m.*
FROM employees m
JOIN org_closure c
  ON c.ancestor_id = m.id
WHERE c.descendant_id = :employee_id
  AND c.depth > 0
ORDER BY c.depth;

フィルタは予測可能になります。例:「リーダーX配下の全員だが部門Yだけ」:

SELECT e.*
FROM employees e
JOIN org_closure c ON c.descendant_id = e.id
WHERE c.ancestor_id = :leader_id
  AND e.department_id = :department_id;

階層が事前計算されているため、件数集計も簡単です(再帰不要)。これによりダッシュボードや権限スコープ合計が扱いやすくなり、ページネーションや検索とも相性が良く、ORDER BYLIMIT/OFFSET、フィルタを直接子孫集合に適用できます。

各モデルが権限とアクセスチェックに与える影響

一般的な組織ルールは単純です:マネージャーは自分の配下のすべてを見ることができる(場合によっては編集もできる)。どのスキーマを選ぶかで「誰が誰の配下か」を判断するコストの頻度が変わります。

隣接リストでは、権限チェックは通常再帰を必要とします。ユーザーが200件の従業員を一覧表示するページを開くとき、配下集合を再帰CTEで作り、それを対象行に対してフィルタすることが多いです。

クロージャーテーブルでは、同じルールを単純な存在チェックで済ませられることが多いです:「閲覧者はこの従業員の祖先か?」という問いを EXISTS (...) で確認すればよいのです。

-- Closure table permission check (conceptual)
SELECT 1
FROM org_closure c
WHERE c.ancestor_id = :viewer_id
  AND c.descendant_id = :employee_id
LIMIT 1;

このシンプルさは、行レベルセキュリティ(RLS)を導入したときに重要になります。RLSではすべてのクエリに「閲覧者が見られる行だけ返す」というルールが自動的に含まれるため、隣接リストだとポリシーに再帰を埋め込む必要がありチューニングが難しくなりますが、クロージャーなら EXISTS によるポリシーがそのまま効きやすいです。

権限ロジックが最も壊れやすいのは以下のようなエッジケースです:

  • 破線(dotted-line)報告:一人が二人のマネージャーを持つ場合。
  • アシスタントや代理:階層に基づかないアクセスは明示的な付与(期限付き)で保存する。
  • 一時的アクセス:有効期間付きの権限は組織構造に埋め込まない。
  • クロスチームプロジェクト:管理チェーンではなくプロジェクト参加でアクセスを付与する。

AppMasterでこれを構築する場合、クロージャーテーブルは視覚的データモデルにきれいにマッピングされ、アクセスチェック自体もWebやモバイルで一貫して単純に保てることが多いです。

トレードオフ:速度、複雑さ、保守

組織データモデルを構築する
PostgreSQL上で従業員、チーム、クロージャーテーブルを視覚的にモデル化します。
AppMasterを試す

何を最適化するかが最大の選択です:単純な書き込みと小さなスキーマにするか、「このマネージャー配下は誰か」を高速に答えられるようにするか。

隣接リストはテーブルが小さく更新が簡単です。一方で読み取り側のコストが出てきます:サブツリーは再帰が必要です。組織が小さい、UIが数レベルしか読み込まない、または階層ベースのフィルタがごく限られた場所のみで使われるなら問題にならないことが多いです。

クロージャーテーブルはそのトレードオフを逆にします。読み取りが高速になり「全ての子孫」への問い合わせが結合で済むようになりますが、移動や再編成時には多くの関係行を挿入・削除する必要が出てきます。

実務では、一般に次のような特徴があります:

  • 読み取り性能:隣接リストは再帰が必要、クロージャーは結合で高速であり組織が大きくなっても安定。
  • 書き込みの複雑さ:隣接リストは parent_id を1つ更新するだけ、クロージャーは移動ごとに多くの行を更新する。
  • データサイズ:隣接リストは人数・チームに比例して増えるが、クロージャーは関係数に比例して増える(最悪ケースでは深い木で概ねN二乗になる)。

インデックスはどちらのモデルでも重要ですが、ターゲットが異なります:

  • 隣接リスト:親ポインタ(manager_id)にインデックスを張り、active フラグなどよく使うフィルタにもインデックスを付ける。
  • クロージャーテーブル:(ancestor_id, descendant_id) の主キーインデックスと、descendant_id 単体のインデックスを用意する。

簡単なルールはこうです:階層でフィルタすることが稀で、権限チェックが「マネージャーは直属の部下を見るだけ」なら隣接リストで十分です。VP X 配下の全員を使ったレポートや部門ツリーでのフィルタ、階層権限を多くの画面で適用するなら、クロージャーテーブルの方が読み取り面で投資に見合うことが多いです。

ステップバイステップ:隣接リストからクロージャーテーブルへ移行する

初日からどちらか一方を選ぶ必要はありません。安全な道筋は、隣接リスト(manager_idparent_id)を残しつつ横にクロージャーテーブルを追加し、徐々に読み取りを移していくことです。これにより新しい階層が実際のクエリや権限チェックでどう振る舞うかを検証しやすくなります。

開始手順の例:

  • クロージャーテーブル(通常 org_closure)とインデックスを作成し、隣接リストは真のソースのままにする。
  • 現在のマネージャー関係からクロージャー行をバックフィルし、自己行(depth=0)も含める。
  • スポットチェックで検証:いくつかのマネージャーを選び、両モデルで配下が一致するか確認する。
  • まず読み取り経路を切り替える:レポート、フィルタ、階層権限は先にクロージャーから読むようにする。
  • 書き込み時にクロージャーも更新するようにし、安定したら再帰クエリを廃止する。

検証時は、アクセスルールで壊れやすいケースに焦点を当ててください:マネージャー変更、最上位リーダー、マネージャーがいないユーザーなど。

AppMasterで構築するなら、古いエンドポイントを残しつつクロージャーから読む新しいエンドポイントを追加し、結果が一致したら切り替えると安全です。

組織フィルタや権限を壊す一般的なミス

チームの稼働環境へデプロイする
内部ツールをAppMaster Cloudかお好みのクラウドプロバイダにデプロイします。
アプリをデプロイ

階層を不整合な状態にしてしまうと、組織機能が簡単に壊れます。データは行ごとには正しく見えても、小さなミスが誤ったフィルタ、遅いページ、あるいは権限漏れを引き起こします。

古典的な問題はサイクルの誤作成です:AがBを管理しており、その後BがAを管理するように設定してしまう(あるいは3〜4人を経由する長いループ)。再帰クエリは無限ループに入るか、重複行を返すか、タイムアウトする可能性があります。クロージャーテーブルでもサイクルは祖先/子孫行を汚染します。

もう一つのよくある問題はクロージャーのズレ(closure drift)です:誰かのマネージャーを変えたが直接関係だけを変えてサブツリーについてのクロージャー行の再構築を忘れてしまう場合です。すると「このVP配下の全員」は古い構造と新しい構造が混在した結果になり、個別のプロフィールページは正しく見えても一覧が壊れることがあります。

部門と報告ラインを明確なルールなしに混ぜると組織図は混乱します。部門は管理上のグルーピングで、報告ラインはマネージャーに関するものと切り分けるとよいでしょう。そうしないと「部門移動」がアクセスに予期せぬ影響を与えることがあります。

権限はしばしば直接のマネージャーだけを見て失敗します。viewer is manager of employee のような単純な条件だと、スキップレベルのマネージャーは見られず過剰にブロックされたり、一時的に直属マネージャーに設定された人が過剰に共有してしまったりします。

遅い一覧ページは多くの場合、毎回再帰的なフィルタを実行していることが原因です(各受信箱、チケット一覧、従業員検索など)。同じフィルタをどこでも使うなら、事前計算されたパス(クロージャーテーブル)か、許可された従業員IDのキャッシュを検討してください。

実践的な安全策:

  • マネージャー変更前にサイクルをブロックするバリデーションを行う。
  • 「部門」の意味を定義し、報告ラインとは分ける。
  • クロージャーを使うならマネージャー変更時に子孫行を再構築する。
  • 権限ルールは直属だけでなくフルチェーンを見るように書く。
  • 一覧ページで使う組織スコープは都度再帰するのではなく事前計算する。

AppMasterで管理パネルを作るなら「マネージャー変更」は敏感なワークフローとして扱い、検証し、関連する階層データを更新してからフィルタやアクセスに反映されるようにしてください。

出荷前の簡単チェックリスト

階層の権限を検証する
シンプルなチェックで画面ごとの「誰が誰を見られるか」を一貫して保ちます。
アクセスをテスト

組織図を「完成」と呼ぶ前に、平易な言葉でアクセスを説明できることを確認してください。誰かが「誰が従業員Xを見られるか、なぜか?」と聞いたら、一つのルールと一つのクエリ(またはビュー)を指し示して証明できるべきです。

次に現実的なパフォーマンスチェックを行います。隣接リストでは「このマネージャー配下の全員を見せてください」が再帰クエリになり、その速度は深さとインデックスに依存します。クロージャーテーブルでは読み取りは通常速いですが、書き込み経路がすべての変更後にテーブルを正しく保つことを信頼できるようにしておく必要があります。

出荷前の短いチェックリスト:

  • 1人の従業員を選び、可視性を端から端までトレースする:どのチェーンがアクセスを付与し、どのロールが拒否するか。
  • 想定規模でマネージャーのサブツリークエリをベンチマークする(例:深さ5、従業員50,000)。
  • 悪い書き込みをブロックする:サイクル、自己管理、孤立ノードをトランザクションと制約で防ぐ。
  • 再編成の安全性をテストする:移動、統合、マネージャー変更、途中で失敗した場合のロールバック。
  • HR、マネージャー、チームリード、サポートなどの現実的なロールについて許可と拒否を断言する権限テストを追加する。

検証の実例:サポート担当者は割り当てられた部門の従業員のみを閲覧でき、マネージャーは自分のフルサブツリーを閲覧できる、というルールをモデル化してテストで証明できれば出荷に近いと言えます。

AppMasterで内部ツールを作るなら、これらのチェックは単なるDBクエリだけでなく、組織リストや従業員プロファイルを返すエンドポイント周りの自動テストとして残してください。

例シナリオと次のステップ

Sales、Support、Engineering の3部門があり、それぞれに2チーム、各チームにリードがいると想定します。Sales Lead A はチームの割引を承認でき、Support Lead B は部門のチケットをすべて見られ、VP of Engineering はEngineering配下をすべて見られます。

その後再編が起きます:あるSupportチームがSalesの下に移り、Salesディレクターと2つのチームリードの間に新しいマネージャーが追加されます。翌日、誰かがアクセスを要求します:「Jamie(Salesのアナリスト)にSales部門の全ての顧客アカウントを見せたいが、Engineeringは見せないでほしい」。

隣接リストで組織図をモデル化するとスキーマはシンプルですが、アプリ側の作業がクエリと権限チェックにシフトします。「Sales配下の全員」のようなフィルタは通常再帰を要します。承認(「チェーン内のマネージャーのみ承認できる」)を追加すると、再編後のエッジケースが重要になります。

クロージャーテーブルでは再編の書き込みは増えます(祖先/子孫行の更新)が、読み取り側は単純になります。フィルタや権限はしばしば単純な結合になります:「このユーザーはあの従業員の祖先か?」や「このチームはこの部門ツリー内か?」といった問いに対応しやすくなります。

これは画面設計にも現れます:部門にスコープされたピッカー、依頼者の上位に最も近いマネージャーへの承認ルーティング、部門ダッシュボード用の管理ビュー、あるいはある日時においてなぜアクセスが存在したかを説明する監査などです。

次のステップ:

  1. 平易な言葉で権限ルールを書き出す(誰が何を見られるか、その理由)。
  2. 最も一般的なチェックに合うモデルを選ぶ(読み取りを速くするか書き込みを単純にするか)。
  3. 再編、アクセス要求、承認をエンドツーエンドでテストできる内部管理ツールを作る。

組織に関する管理パネルやポータルを迅速に構築したい場合は、AppMaster(appmaster.io)が実用的な選択肢になりえます:PostgreSQLバックエンドのデータをモデル化し、視覚的なビジネスプロセスで承認ロジックを実装し、同じバックエンドからWebとネイティブモバイルアプリを提供できます。

よくある質問

隣接リストとクロージャーテーブルはいつ使い分けるべきですか?

組織が小さく、更新が頻繁で、ほとんどの画面が直属の部下や数レベルだけを必要とする場合は隣接リストを使ってください。逆に「このリーダー配下の全員」を常に求める、部門ごとのフィルタや複数の画面にまたがる階層ベースの権限を頻繁に使う場合はクロージャーテーブルを検討してください。読み取りが単純な結合で済み、スケールしても予測しやすくなります。

PostgreSQLで「誰が誰に報告しているか」を保存する最も簡単な方法は?

まずは employees(manager_id) のように manager_id を保持するのが最も簡単です。直属の部下は WHERE manager_id = ? で取得できます。承認や「自分の組織」フィルタ、スキップレベルのダッシュボードなど全系譜や全サブツリーが本当に必要になったら再帰クエリを追加してください。

サイクル(AがBを管理し、BがAを管理する)をどう防げばいいですか?

自己管理を防ぐチェック(例: manager_id \u003c\u003e id)を入れ、更新時には割り当てるマネージャーが既にその従業員のサブツリーに含まれていないか検証してください。実務では、マネージャー変更を保存する前に祖先関係のチェックを行うのが最も安全で、1つのサイクルが再帰を壊し権限ロジックを汚染します。

部門は人と同じ階層ノードにすべきですか?

良いデフォルトは、部門は管理上のグルーピング、報告ラインは別のマネージャーツリーとして扱うことです。こうすると「部門移動」が誰に報告するかを勝手に変えてしまうことを避けられ、報告ラインと部門の境界が一致しない場合でも「Salesの全員」といったフィルタが明確になります。

複数のマネージャーがいるマトリックス組織はどうモデル化すべきですか?

通常は従業員テーブルに主な報告先(primary)を持ち、破線(dotted-line)の関係は別テーブルで表現します。こうすると基本的な階層クエリを壊さずにプロジェクトアクセスや承認の委譲といった特別なルールを実装できます。

誰かのマネージャーが変わったとき、クロージャーテーブルで何を更新する必要がありますか?

移動する従業員のサブツリーに対する古い祖先パスを削除し、新しいマネージャーの祖先とサブツリー内の全ノードを組み合わせて新しいパスを挿入し、depth を再計算します。途中で失敗してクロージャーテーブルが半分だけ更新されないよう、トランザクション内で行ってください。

組織図クエリで重要なインデックスは何ですか?

隣接リストではほとんどの組織クエリが employees(manager_id) に依存するので、manager_id にインデックスを張るのが重要です。クロージャーテーブルでは (ancestor_id, descendant_id) に対する主キーインデックスと、descendant_id 単体のインデックスが「この行を誰が見られるか?」といったチェックを速くします。

「マネージャーは配下全員を見られる」を安全に実装するには?

よく使われるパターンはクロージャーテーブル上の EXISTS チェックです。閲覧者が対象従業員の祖先であればアクセスを許可する、というものはRLSと相性がよく、アプリ側のあちこちで同じ再帰ロジックを再実装する必要がなくなります。

SELECT 1
FROM org_closure c
WHERE c.ancestor_id = :viewer_id
  AND c.descendant_id = :employee_id
LIMIT 1;
再編履歴や監査ログはどう扱うべきですか?

変更履歴は通常、別テーブルに有効日付きで記録しておくのが良いです。現在の manager を上書きして過去を失うのではなく、誰がいつ誰に報告していたかを答えられるようにしておくと、再編成後の集計や監査が安定します。

隣接リストからクロージャーテーブルへ、アプリを壊さずに移行する方法は?

既存の manager_id を真のソースとして残し、並行してクロージャーテーブルを作成してバックフィルしてください。まずは読み取り側を切り替え(レポートやフィルタ、権限チェックをクロージャーから読む)、次に書き込みで両方を更新し、現場で結果が一致することを確認してから古い再帰クエリを廃止します。

始めやすい
何かを作成する 素晴らしい

無料プランで AppMaster を試してみてください。
準備が整ったら、適切なサブスクリプションを選択できます。

始める