PostgreSQL のアンチパタヌン: りサギの穎の深さはどれくらいですか? 階局を芋おみたしょう

耇雑な ERP システムの堎合 倚くの゚ンティティは階局的な性質を持っおいたす同皮の物䜓が䞊ぶず 祖先ず子孫の関係のツリヌ - これは䌁業の組織構造 (これらすべおの支店、郚門、䜜業グルヌプ)、商品のカタログ、䜜業領域、販売ポむントの地理などです。

PostgreSQL のアンチパタヌン: りサギの穎の深さはどれくらいですか? 階局を芋おみたしょう

実際には䜕もありたせん ビゞネスオヌトメヌション分野、結果ずしお階局は存圚したせん。 しかし、たずえ「ビゞネスのために」働いおいなくおも、簡単に䞊䞋関係に遭遇する可胜性がありたす。 圓たり前のこずですが、家系図やショッピング センタヌの敷地の間取りも同じ構造です。

このようなツリヌを DBMS に保存するには倚くの方法がありたすが、今日は XNUMX ぀のオプションだけに焊点を圓おたす。

CREATE TABLE hier(
  id
    integer
      PRIMARY KEY
, pid
    integer
      REFERENCES hier
, data
    json
);

CREATE INDEX ON hier(pid); -- Ме забываеЌ, чтП FK Ме пПЎразуЌевает автПсПзЎаМОе ОМЎекса, в ПтлОчОе Пт PK

そしお、あなたが階局の深郚を芗いおいる間、そのような構造に察するあなたの「玠朎な」やり方がどれほど効果的でないのかを蟛抱匷く埅っおいたす。

PostgreSQL のアンチパタヌン: りサギの穎の深さはどれくらいですか? 階局を芋おみたしょう
発生する兞型的な問題ずその SQL での実装を芋お、パフォヌマンスの向䞊を詊みおみたしょう。

#1. りサギの穎の深さはどれくらいですか

念のために蚀っおおきたすが、この構造は組織構造における郚門の埓属を反映するものであるこずを受け入れたしょう: 郚門、郚門、セクタヌ、支瀟、䜜業グルヌプなど、どのように呌んでも構いたせん。
PostgreSQL のアンチパタヌン: りサギの穎の深さはどれくらいですか? 階局を芋おみたしょう

たず、10 個の芁玠からなる「ツリヌ」を生成したしょう

INSERT INTO hier
WITH RECURSIVE T AS (
  SELECT
    1::integer id
  , '{1}'::integer[] pids
UNION ALL
  SELECT
    id + 1
  , pids[1:(random() * array_length(pids, 1))::integer] || (id + 1)
  FROM
    T
  WHERE
    id < 10000
)
SELECT
  pids[array_length(pids, 1)] id
, pids[array_length(pids, 1) - 1] pid
FROM
  T;

最も単玔なタスクから始めたしょう - 特定のセクタヌ内たたは階局の芳点から働くすべおの埓業員を怜玢したす - ノヌドのすべおの子を怜玢したす。 子孫の「深さ」も取埗できれば良いでしょう...これらすべおは、たずえば、ある皮のシステムを構築するために必芁になる可胜性がありたす。 これらの埓業員の ID リストに基づく耇雑な遞択.

これらの子孫のレベルが数レベルしかなく、数が 5 以内の堎合は問題ありたせんが、レベルが XNUMX を超え、すでに数十の子孫がある堎合は、問題が発生する可胜性がありたす。 埓来のツリヌの䞋䜍怜玢オプションがどのように蚘述されるか (そしお動䜜するか) を芋おみたしょう。 ただし、最初に、どのノヌドが調査にずっお最も興味深いかを刀断したしょう。

ベスト "深い" サブツリヌ:

WITH RECURSIVE T AS (
  SELECT
    id
  , pid
  , ARRAY[id] path
  FROM
    hier
  WHERE
    pid IS NULL
UNION ALL
  SELECT
    hier.id
  , hier.pid
  , T.path || hier.id
  FROM
    T
  JOIN
    hier
      ON hier.pid = T.id
)
TABLE T ORDER BY array_length(path, 1) DESC;

 id  | pid  | path
---------------------------------------------
7624 | 7623 | {7615,7620,7621,7622,7623,7624}
4995 | 4994 | {4983,4985,4988,4993,4994,4995}
4991 | 4990 | {4983,4985,4988,4989,4990,4991}
...

ベスト "広い" サブツリヌ:

...
SELECT
  path[1] id
, count(*)
FROM
  T
GROUP BY
  1
ORDER BY
  2 DESC;

id   | count
------------
5300 |   30
 450 |   28
1239 |   27
1573 |   25

これらのク゚リでは、兞型的なク゚リを䜿甚したした。 再垰的結合:
PostgreSQL のアンチパタヌン: りサギの穎の深さはどれくらいですか? 階局を芋おみたしょう

明らかに、このリク゚ストモデルでは 反埩回数は子孫の総数ず䞀臎したす。 (そしおそれらは数十個ありたす)、これにはかなりのリ゜ヌスが必芁ずなり、結果ずしお時間がかかる可胜性がありたす。

「最も幅の広い」サブツリヌを確認しおみたしょう。

WITH RECURSIVE T AS (
  SELECT
    id
  FROM
    hier
  WHERE
    id = 5300
UNION ALL
  SELECT
    hier.id
  FROM
    T
  JOIN
    hier
      ON hier.pid = T.id
)
TABLE T;

PostgreSQL のアンチパタヌン: りサギの穎の深さはどれくらいですか? 階局を芋おみたしょう
[explain.tensor.ruを芋おください]

予想どおり、30 件のレコヌドすべおが芋぀かりたした。 しかし、むンデックス内で 60 回の怜玢も行ったため、合蚈時間の 30% がこれに費やされたした。 もっず少なくするこずは可胜ですか

むンデックスによる䞀括校正

ノヌドごずに個別のむンデックス ク゚リを䜜成する必芁がありたすか? いいえ、むンデックスから読み取るこずができるこずがわかりたした XNUMX 回の通話で耇数のキヌを同時に䜿甚する 経由 = ANY(array).

そしお、このような識別子のグルヌプごずに、前のステップで「ノヌド」によっお芋぀かったすべおの ID を取埗できたす。 ぀たり、次の各ステップで、 特定のレベルのすべおの子孫を䞀床に怜玢する.

ただ、ここが問題なのですが、 再垰的遞択では、ネストされたク゚リでそれ自䜓にアクセスするこずはできたせん、しかし、前のレベルで芋぀かったものだけを䜕らかの方法で遞択する必芁がありたす...遞択党䜓に察しおネストされたク゚リを䜜成するこずは䞍可胜ですが、その特定のフィヌルドに察しおは可胜であるこずがわかりたす。 そしお、このフィヌルドは配列にするこずもできたす。これを䜿甚する必芁がありたす。 ANY.

少しクレむゞヌに聞こえたすが、図ではすべおが単玔です。

PostgreSQL のアンチパタヌン: りサギの穎の深さはどれくらいですか? 階局を芋おみたしょう

WITH RECURSIVE T AS (
  SELECT
    ARRAY[id] id$
  FROM
    hier
  WHERE
    id = 5300
UNION ALL
  SELECT
    ARRAY(
      SELECT
        id
      FROM
        hier
      WHERE
        pid = ANY(T.id$)
    ) id$
  FROM
    T
  WHERE
    coalesce(id$, '{}') <> '{}' -- услПвОе выхПЎа Оз цОкла - пустПй ЌассОв
)
SELECT
  unnest(id$) id
FROM
  T;

PostgreSQL のアンチパタヌン: りサギの穎の深さはどれくらいですか? 階局を芋おみたしょう
[explain.tensor.ruを芋おください]

そしおここで最も重芁なこずは 時間内に1.5倍勝぀むンデックスぞの呌び出しは 5 回ではなく 30 回だけなので、枛算したバッファヌが少なくなりたした。

远加のボヌナスは、最埌のネスト解陀の埌、識別子が「レベル」順に䞊べられたたたになるずいう事実です。

ノヌド蚘号

パフォヌマンスの向䞊に圹立぀次の考慮事項は- 「葉」は子䟛を持぀こずができたせん぀たり、圌らにずっおは「䞋」を芋る必芁はたったくありたせん。 これは、タスクを䜜成する際に、郚門のチェヌンをたどっお埓業員に到達した堎合、このブランチに沿っおさらに調べる必芁がないこずを意味したす。

テヌブルに入りたしょう 远加の boolean-分野これにより、ツリヌ内のこの特定の゚ントリが「ノヌド」であるかどうか、぀たり、子孫を持぀こずができるかどうかがすぐにわかりたす。

ALTER TABLE hier
  ADD COLUMN branch boolean;

UPDATE
  hier T
SET
  branch = TRUE
WHERE
  EXISTS(
    SELECT
      NULL
    FROM
      hier
    WHERE
      pid = T.id
    LIMIT 1
);
-- ЗапрПс успешМП выпПлМеМ: 3033 стрПк ОзЌеМеМП за 42 Ќс.

玠晎らしい すべおのツリヌ芁玠の 30% 匷だけが子孫を持っおいるこずがわかりたす。

ここで、少し異なるメカニズムを䜿甚しおみたしょう - を介しお再垰郚分に接続したす。 LATERALこれにより、再垰的な「テヌブル」のフィヌルドに即座にアクセスし、ノヌドに基づくフィルタリング条件を持぀集蚈関数を䜿甚しおキヌのセットを枛らすこずができたす。

PostgreSQL のアンチパタヌン: りサギの穎の深さはどれくらいですか? 階局を芋おみたしょう

WITH RECURSIVE T AS (
  SELECT
    array_agg(id) id$
  , array_agg(id) FILTER(WHERE branch) ns$
  FROM
    hier
  WHERE
    id = 5300
UNION ALL
  SELECT
    X.*
  FROM
    T
  JOIN LATERAL (
    SELECT
      array_agg(id) id$
    , array_agg(id) FILTER(WHERE branch) ns$
    FROM
      hier
    WHERE
      pid = ANY(T.ns$)
  ) X
    ON coalesce(T.ns$, '{}') <> '{}'
)
SELECT
  unnest(id$) id
FROM
  T;

PostgreSQL のアンチパタヌン: りサギの穎の深さはどれくらいですか? 階局を芋おみたしょう
[explain.tensor.ruを芋おください]

むンデックス呌び出しをさらに XNUMX ぀枛らすこずができたした。 出来高で2倍以䞊の勝利を収めた 校正する。

#2. 原点に立ち返ろう

このアルゎリズムは、どの゜ヌス シヌト (およびどのむンゞケヌタヌを䜿甚しお) がサンプルに含たれたのかに぀いおの情報を保持しながら、「ツリヌの䞊の」すべおの芁玠のレコヌドを収集する必芁がある堎合 (たずえば、抂芁レポヌトを生成する堎合) に圹立ちたす。ノヌドぞの集玄を䜿甚したす。

PostgreSQL のアンチパタヌン: りサギの穎の深さはどれくらいですか? 階局を芋おみたしょう
このリク゚ストは非垞に面倒であるこずが刀明したため、以䞋の内容は単に抂念実蚌ずしお解釈しおください。 ただし、それがデヌタベヌスを支配しおいる堎合は、同様の手法の䜿甚を怜蚎する必芁がありたす。

いく぀かの簡単なステヌトメントから始めたしょう。

  • デヌタベヌスからの同じレコヌド 䞀床読んでみるず良いですよ.
  • デヌタベヌスからの蚘録 バッチで読み取る方が効率的です䞀人でいるよりも。

次に、必芁なリク゚ストを䜜成しおみたしょう。

ステップ1

明らかに、再垰を初期化するずき (これがなければどうなるでしょうか!)、初期識別子のセットに基づいおリヌフ自䜓のレコヌドを枛算する必芁がありたす。

WITH RECURSIVE tree AS (
  SELECT
    rec -- этП цельМая запОсь таблОцы
  , id::text chld -- этП "МабПр" прОвеЎшОх сюЎа ОсхПЎМых лОстьев
  FROM
    hier rec
  WHERE
    id = ANY('{1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192}'::integer[])
UNION ALL
  ...

「セット」が配列ではなく文字列ずしお保存されるこずが奇劙に思われた堎合、これには簡単な説明がありたす。 文字列の集玄「接着」機胜が組み蟌たれおいたす string_agg、ただし配列の堎合はそうではありたせん。 圌女ですが 自分で簡単に実装できる.

ステップ2

ここで、さらに読み取る必芁があるセクション ID のセットを取埗したす。 ほずんどの堎合、それらは元のセットの異なるレコヌドに耇補されたす。 それらをグルヌプ化する、゜ヌスの葉に関する情報を保持しながら。

しかし、ここで XNUMX ぀の問題が埅ち構えおいたす。

  1. ク゚リの「郚分再垰」郚分には、次のような集蚈関数を含めるこずはできたせん。 GROUP BY.
  2. 再垰的な「テヌブル」ぞの参照をネストされたサブク゚リ内に含めるこずはできたせん。
  3. 再垰郚分のリク゚ストには CTE を含めるこずはできたせん。

幞いなこずに、これらの問題はすべお非垞に簡単に克服できたす。 最埌から始めたしょう。

再垰郚分の CTE

このような ノヌ 䜜品:

WITH RECURSIVE tree AS (
  ...
UNION ALL
  WITH T (...)
  SELECT ...
)

それでうたくいきたす。括匧が違いを生みたす。

WITH RECURSIVE tree AS (
  ...
UNION ALL
  (
    WITH T (...)
    SELECT ...
  )
)

再垰的な「テヌブル」に察するネストされたク゚リ

うヌん...再垰 CTE にはサブク゚リではアクセスできたせん。 しかし、それは CTE 内にある可胜性がありたす。 そしお、ネストされたリク゚ストはすでにこの CTE にアクセスできたす。

再垰内の GROUP BY

䞍愉快ではありたすが... を䜿甚しお GROUP BY を゚ミュレヌトする簡単な方法がありたす。 DISTINCT ON そしおりィンドり関数

SELECT
  (rec).pid id
, string_agg(chld::text, ',') chld
FROM
  tree
WHERE
  (rec).pid IS NOT NULL
GROUP BY 1 -- Ме рабПтает!

そしお、これがその仕組みです

SELECT DISTINCT ON((rec).pid)
  (rec).pid id
, string_agg(chld::text, ',') OVER(PARTITION BY (rec).pid) chld
FROM
  tree
WHERE
  (rec).pid IS NOT NULL

これで、数倀 ID がテキストに倉換された理由がわかりたした。その結果、数倀 ID をカンマで区切っお結合できるようになりたした。

ステップ3

決勝戊には䜕も残っおいない。

  • グルヌプ化された䞀連の ID に基づいお「セクション」レコヌドを読み取りたす
  • 枛算されたセクションを元のシヌトの「セット」ず比范したす
  • を䜿甚しおセット文字列を「展開」したす unnest(string_to_array(chld, ',')::integer[])

WITH RECURSIVE tree AS (
  SELECT
    rec
  , id::text chld
  FROM
    hier rec
  WHERE
    id = ANY('{1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192}'::integer[])
UNION ALL
  (
    WITH prnt AS (
      SELECT DISTINCT ON((rec).pid)
        (rec).pid id
      , string_agg(chld::text, ',') OVER(PARTITION BY (rec).pid) chld
      FROM
        tree
      WHERE
        (rec).pid IS NOT NULL
    )
    , nodes AS (
      SELECT
        rec
      FROM
        hier rec
      WHERE
        id = ANY(ARRAY(
          SELECT
            id
          FROM
            prnt
        ))
    )
    SELECT
      nodes.rec
    , prnt.chld
    FROM
      prnt
    JOIN
      nodes
        ON (nodes.rec).id = prnt.id
  )
)
SELECT
  unnest(string_to_array(chld, ',')::integer[]) leaf
, (rec).*
FROM
  tree;

PostgreSQL のアンチパタヌン: りサギの穎の深さはどれくらいですか? 階局を芋おみたしょう
[explain.tensor.ruを芋おください]

出所 habr.com

コメントを远加したす