PostgreSQL Antipatterns: қоян тесігі қаншалықты терең? иерархия арқылы өтейік

Күрделі ERP жүйелерінде көптеген нысандар иерархиялық сипатқа иебіртекті заттар қатар тұрғанда ата-баба қарым-қатынасының ағашы - бұл кәсіпорынның ұйымдық құрылымы (барлық осы филиалдар, бөлімдер мен жұмыс топтары), және тауарлар каталогы, және жұмыс бағыттары, және сату нүктелерінің географиясы,...

PostgreSQL Antipatterns: қоян тесігі қаншалықты терең? иерархия арқылы өтейік

Шын мәнінде, жоқ бизнесті автоматтандыру аймақтары, нәтижесінде ешқандай иерархия болмайды. Бірақ сіз «бизнес үшін» жұмыс істемесеңіз де, иерархиялық қатынастарды оңай кездестіре аласыз. Бұл қарапайым, тіпті сіздің отбасылық ағашыңыз немесе сауда орталығындағы үй-жайлардың жоспары бірдей құрылым.

Мұндай ағашты ДҚБЖ-да сақтаудың көптеген жолдары бар, бірақ бүгін біз тек бір нұсқаға тоқталамыз:

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

CREATE INDEX ON hier(pid); -- не забываем, что FK не подразумевает автосоздание индекса, в отличие от PK

Сіз иерархияның тереңдігіне үңіліп жатқанда, мұндай құрылыммен жұмыс істеудің «аңғал» әдістерінің қаншалықты тиімді болатынын шыдамдылықпен күтеді.

PostgreSQL Antipatterns: қоян тесігі қаншалықты терең? иерархия арқылы өтейік
Пайда болатын типтік мәселелерді, олардың SQL жүйесінде орындалуын қарастырайық және олардың өнімділігін жақсартуға тырысайық.

№1. Қоянның шұңқыры қаншалықты терең?

Бұл құрылым ұйым құрылымындағы басқармалардың: департаменттердің, бөлімдердің, секторлардың, филиалдардың, жұмыс топтарының... - оларды қалай атасаңыз да, бағыныстылығын көрсететінін нақтылық үшін қабылдаймыз.
PostgreSQL Antipatterns: қоян тесігі қаншалықты терең? иерархия арқылы өтейік

Алдымен 10K элементтен тұратын «ағашты» жасайық

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;

Ең қарапайым тапсырмадан бастайық - белгілі бір секторда немесе иерархия тұрғысынан жұмыс істейтін барлық қызметкерлерді табу - түйіннің барлық еншілестерін табыңыз. Ұрпақтың «тереңдігін» алу да жақсы болар еді... Мұның бәрі, мысалы, қандай да бір құрылыс салу үшін қажет болуы мүмкін. осы қызметкерлердің жеке куәліктерінің тізімі негізінде кешенді таңдау.

Бұл ұрпақтардың бірнеше деңгейі болса және саны онға жуық болса, бәрі жақсы болар еді, бірақ 5 деңгейден көп болса және ондаған ұрпақтар болса, проблемалар болуы мүмкін. Дәстүрлі іздеу опциялары қалай жазылғанын (және жұмыс істейтінін) қарастырайық. Бірақ алдымен біздің зерттеуіміз үшін қандай түйіндер ең қызықты болатынын анықтайық.

Ең көп «терең» ішкі ағаштар:

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

Бұл сұраулар үшін біз әдеттегіні қолдандық рекурсивті JOIN:
PostgreSQL Antipatterns: қоян тесігі қаншалықты терең? иерархия арқылы өтейік

Әлбетте, бұл сұраныс үлгісімен қайталану саны ұрпақтардың жалпы санына сәйкес келеді (және олардың бірнеше ондағандары бар) және бұл айтарлықтай ресурстарды және нәтижесінде уақытты қажет етуі мүмкін.

«Ең кең» ішкі ағашты тексерейік:

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 Antipatterns: қоян тесігі қаншалықты терең? иерархия арқылы өтейік
[express.tensor.ru сайтынан қарау]

Күткендей, біз барлық 30 жазбаны таптық. Бірақ олар бұған жалпы уақыттың 60% жұмсады - өйткені олар индексте 30 іздеу жасады. Азырақ істеуге бола ма?

Индекс бойынша жаппай тексеру

Әрбір түйін үшін жеке индекстік сұрау жасау керек пе? Жоқ екен – көрсеткіштен оқи аламыз бір қоңырауда бірден бірнеше пернені пайдалану көмегімен = ANY(array).

Әрбір осындай идентификаторлар тобында біз «түйіндер» бойынша алдыңғы қадамда табылған барлық идентификаторларды қабылдай аламыз. Яғни, әрбір келесі қадамда біз жасаймыз белгілі бір деңгейдегі барлық ұрпақтарды бірден іздеу.

Тек, мәселе осында, рекурсивті таңдауда кірістірілген сұрауда өзіне қол жеткізе алмайсыз, бірақ біз қандай да бір жолмен тек алдыңғы деңгейде табылғанды ​​таңдауымыз керек... Бүкіл таңдау үшін кірістірілген сұраныс жасау мүмкін емес, бірақ оның нақты өрісі үшін бұл мүмкін. Және бұл өріс массив болуы мүмкін - бұл бізге пайдалануымыз керек ANY.

Бұл аздап ақылсыз болып көрінеді, бірақ диаграммада бәрі қарапайым.

PostgreSQL Antipatterns: қоян тесігі қаншалықты терең? иерархия арқылы өтейік

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 Antipatterns: қоян тесігі қаншалықты терең? иерархия арқылы өтейік
[express.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 Antipatterns: қоян тесігі қаншалықты терең? иерархия арқылы өтейік

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 Antipatterns: қоян тесігі қаншалықты терең? иерархия арқылы өтейік
[express.tensor.ru сайтынан қарау]

Біз тағы бір индекстік қоңырауды азайта алдық және көлемі бойынша 2 еседен астам жеңіске жетті түзету.

№2. Тамырларға қайта оралайық

Бұл алгоритм қай бастапқы парақ (және қандай көрсеткіштермен) үлгіге қосылуына себеп болғаны туралы ақпаратты сақтай отырып, «ағаштың үстінде» барлық элементтер үшін жазбаларды жинау қажет болса, пайдалы болады - мысалы, жиынтық есепті құру түйіндерге біріктіру арқылы.

PostgreSQL Antipatterns: қоян тесігі қаншалықты терең? иерархия арқылы өтейік
Төмендегілерді тек тұжырымдаманың дәлелі ретінде қабылдау керек, өйткені сұрау өте ауыр болып шығады. Бірақ егер ол сіздің дерекқорыңызда басым болса, ұқсас әдістерді пайдалану туралы ойлануыңыз керек.

Бірнеше қарапайым мәлімдемелерден бастайық:

  • Деректер базасынан бірдей жазба Бір рет оқыған дұрыс.
  • Дерекқордан алынған жазбалар Топтамамен оқу тиімдірекжалғыз қарағанда.

Енді бізге қажетті сұранысты құрастырып көрейік.

қадам 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

Енді біз одан әрі оқуды қажет ететін бөлім идентификаторларының жинағын аламыз. Әрқашан дерлік олар бастапқы жинақтың әртүрлі жазбаларында қайталанатын болады - біз солай болар едік оларды топтастыру, бастапқы жапырақтар туралы ақпаратты сақтай отырып.

Бірақ бұл жерде бізді үш қиындық күтіп тұр:

  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

Енді біз сандық идентификатордың неліктен мәтінге айналғанын көреміз - осылайша оларды үтір арқылы біріктіруге болады!

қадам 3

Финалға бізде ештеңе қалмады:

  • топтастырылған идентификаторлар жиынтығына негізделген «бөлім» жазбаларын оқимыз
  • біз шегерілген бөлімдерді бастапқы парақтардың «жиындарымен» салыстырамыз
  • көмегімен жиынтық жолды «кеңейту». 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 Antipatterns: қоян тесігі қаншалықты терең? иерархия арқылы өтейік
[express.tensor.ru сайтынан қарау]

Ақпарат көзі: www.habr.com

пікір қалдыру