PostgreSQL Antipatterns: wie tief geht der Kaninchenbau? Lassen Sie uns die Hierarchie durchgehen.

In komplexen ERP-Systemen haben viele Entitäten eine hierarchische Natur., wenn homogene Objekte in einen Baum von Beziehungen „Elternteil - Nachkomme“ angeordnet werden. Dies umfasst sowohl die organisatorische Struktur des Unternehmens (alle diese Niederlassungen, Abteilungen und Arbeitsgruppen) als auch den Produktkatalog, die Arbeitsbereiche und die geografischen Standorte der Verkaufsstellen,…

PostgreSQL Antipatterns: wie tief geht der Kaninchenbau? Lassen Sie uns die Hierarchie durchgehen.

Tatsächlich gibt es keinen Bereich der Automatisierung von Geschäftsprozessen,in dem nicht irgendwie eine Hierarchie vorhanden ist. Aber selbst wenn Sie nicht im „Geschäft“ arbeiten, können Sie leicht mit hierarchischen Beziehungen konfrontiert werden. Einfach gesagt, selbst Ihr Stammbaum oder der Grundriss eines Einkaufszentrums ist eine ähnliche Struktur.

Es gibt viele Möglichkeiten, einen solchen Baum in einer DBMS zu speichern, aber heute konzentrieren wir uns nur auf eine Möglichkeit:

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

CREATE INDEX ON hier(pid); -- Denken Sie daran, dass der FK nicht automatisch einen Index erstellt, im Gegensatz zum PK.

Und während Sie in die Tiefe der Hierarchie schauen, wartet sie geduldig, wie ineffektiv Ihre 'naiven' Ansätze im Umgang mit einer solchen Struktur sein werden.

PostgreSQL Antipatterns: wie tief geht der Kaninchenbau? Lassen Sie uns die Hierarchie durchgehen.
Lassen Sie uns typische auftretende Aufgaben besprechen, deren Umsetzung in SQL analysieren und versuchen, deren Leistung zu verbessern.

#1. Насколько глубока кроличья нора?

Nehmen wir zur Klarheit an, dass diese Struktur die Unterordnung der Abteilungen in der Organisationsstruktur widerspiegelt: Abteilungen, Divisionen, Sektoren, Zweigstellen, Arbeitsgruppen,... ganz gleich, wie man sie nennt.
PostgreSQL Antipatterns: wie tief geht der Kaninchenbau? Lassen Sie uns die Hierarchie durchgehen.

Zuerst generieren wir unser 'Baum'-Modell mit 10K Elementen.

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;

Beginnen wir mit der einfachsten Aufgabe – alle Mitarbeiter zu finden, die innerhalb eines bestimmten Sektors arbeiten, oder in hierarchischen Begriffen – alle Nachkommen des Knotens finden.. Und es wäre auch gut, die 'Tiefe' des Nachkommen zu erhalten... All dies kann notwendig sein, zum Beispiel zur Erstellung einer komplexen Abfrage basierend auf der Liste der IDs dieser Mitarbeiter..

Alles wäre in Ordnung, wenn es dort nur ein paar Ebenen und insgesamt nur ein Dutzend Nachkommen gäbe, aber wenn es mehr als 5 Ebenen sind und bereits Dutzende von Nachkommen existieren, können Probleme auftreten. Lassen Sie uns betrachten, wie die traditionellen Varianten der „Baumabwärtsuche“ geschrieben (und funktionieren) gehören. Aber zuerst definieren wir, welche Knoten für unsere Untersuchungen am interessantesten sind.

Die meisten „tiefen“ Unterbäume:

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}
...

Die meisten „breite“ Unterbäume:

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

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

Für diese Abfragen haben wir einen typischen rekursiven JOIN:
PostgreSQL Antipatterns: wie tief geht der Kaninchenbau? Lassen Sie uns die Hierarchie durchgehen.

Offensichtlich wird bei einem solchen Abfragemodell die Anzahl der Iterationen mit der Gesamtzahl der Nachkommen übereinstimmen (und da gibt es mehrere Dutzend), und das kann beträchtliche Ressourcen und folglich Zeit beanspruchen.

Überprüfen wir das am „breitesten“ Unterbaum:

MIT REKURSION T ALS (
  AUSWÄHLEN
    id
  VON
    hier
  WO
    id = 5300
VEREINIGUNG ALL (
  AUSWÄHLEN
    hier.id
  VON
    T
  JOIN
    hier
      ON hier.pid = T.id
)
TABELLE T;

PostgreSQL Antipatterns: wie tief geht der Kaninchenbau? Lassen Sie uns die Hierarchie durchgehen.
[sehen Sie sich explain.tensor.ru an]

Wie erwartet haben wir alle 30 Datensätze gefunden. Aber wir haben dafür 60 % der gesamten Zeit gebraucht – weil wir dabei auch 30 Indexpfaden gemacht haben. Geht es nicht schneller?

Massenabgleich nach Index

Müssen wir für jeden Knoten eine separate Anfrage an den Index stellen? Es stellt sich heraus, dass wir dies nicht tun müssen – wir können gleichzeitig über mehrere Schlüssel mit einem Zugriff lesen. mit Hilfe von = ANY(array).

In jede solche Gruppe von Identifikatoren können wir alle in der vorherigen Stufe gefundenen IDs nach „Knoten“ einfügen. Das heißt, in jedem nächsten Schritt werden wir sofort alle Nachkommen einer bestimmten Ebene suchen..

Nur ist das Problem, dass wir in der rekursiven Auswahl nicht auf uns selbst im verschachtelten Abfrage zugreifen können,, aber wir müssen ja irgendwie nur das auswählen, was auf der vorhergehenden Ebene gefunden wurde... Es stellt sich heraus, dass man keine verschachtelte Anfrage für die gesamte Auswahl machen kann, sondern für ein bestimmtes Feld – und dieses Feld kann auch ein Array sein – das ist genau das, was wir für die Nutzung benötigen. ANY.

Das klingt etwas verrückt, aber auf dem Schema – ist alles einfach.

PostgreSQL Antipatterns: wie tief geht der Kaninchenbau? Lassen Sie uns die Hierarchie durchgehen.

MIT REKURSIVEN T AS (
  WÄHLEN
    ARRAY[id] id$
  VON
    hier
  WO
    id = 5300
UNION ALL
  WÄHLEN
    ARRAY(
      WÄHLEN
        id
      VON
        hier
      WO
        pid = ANY(T.id$)
    ) id$
  VON
    T
  WO
    coalesce(id$, '{}') >> '{}' -- Bedingung für das Verlassen der Schleife - leeres Array
)
WÄHLEN
  unnest(id$) id
VON
  T;

PostgreSQL Antipatterns: wie tief geht der Kaninchenbau? Lassen Sie uns die Hierarchie durchgehen.
[sehen Sie sich explain.tensor.ru an]

Hier ist das Wichtigste nicht einmal der Gewinn von 1,5-facher Zeit, sondern dass wir weniger Buffers abgezogen haben, da wir nur 5 statt 30 Zugriffe auf den Index hatten!

Ein zusätzlicher Vorteil ist, dass nach dem endgültigen unnest die Identifikatoren nach "Ebenen" geordnet bleiben.

Knotensignal

Der nächste Gedanke, der hilft, die Leistung zu verbessern - „Blätter“ können keine Kinder haben, das bedeutet, dass wir für sie nach „unten“ überhaupt nicht suchen müssen. In der Formulierung unserer Aufgabe bedeutet dies, dass, wenn wir einer Kette von Abteilungen gefolgt sind und beim Mitarbeiter angelangt sind, es nicht mehr nötig ist, weiter in diesem Ast zu suchen.

Lassen Sie uns in unsere Tabelle ein zusätzliches boolean-Feldeinführen, das uns sofort sagt, ob dieser spezifische Datensatz in unserem Baum ein „Knoten“ ist - das heißt, ob er überhaupt Nachkommen haben kann.

ÄNDERN SIE TABELLE hier
  FÜGE SPALTE branch boolean HINZU;

UPDATE
  hier T
SET
  branch = TRUE
WHERE
  EXISTS(
    SELECT
      NULL
    FROM
      hier
    WHERE
      pid = T.id
    LIMIT 1
);
-- Anfrage erfolgreich ausgeführt: 3033 Zeilen in 42 ms geändert.

Großartig! Es stellt sich heraus, dass wir nur etwas mehr als 30 % aller Elemente im Baum Nachkommen haben.

Jetzt wenden wir eine etwas andere Mechanik an – Verbindungen mit dem rekursiven Teil über LATERAL, was es uns ermöglicht, direkt auf die Felder der rekursiven „Tabelle“ zuzugreifen, und die Aggregatfunktion mit der Filterbedingung nach dem Knotenmerkmal verwenden wir, um die Schlüsselauswahl zu reduzieren:

PostgreSQL Antipatterns: wie tief geht der Kaninchenbau? Lassen Sie uns die Hierarchie durchgehen.

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: wie tief geht der Kaninchenbau? Lassen Sie uns die Hierarchie durchgehen.
[sehen Sie sich explain.tensor.ru an]

Wir konnten einen weiteren Zugriff auf den Index reduzieren und über 2 Mal bei der Menge gewonnen der gelesenen Daten.

#2. Вернемся к корням

Dieser Algorithmus ist nützlich, wenn Sie Aufzeichnungen für alle Elemente "nach oben im Baum" sammeln müssen, während Sie die Informationen beibehalten, welches Ausgangsblatt (und mit welchen Kennzahlen) zu dessen Aufnahme in die Auswahl geführt hat – zum Beispiel zur Erstellung eines konsolidierten Berichts mit Aggregation auf Knoten.

PostgreSQL Antipatterns: wie tief geht der Kaninchenbau? Lassen Sie uns die Hierarchie durchgehen.
Das Folgende ist lediglich als Proof-of-Concept zu verstehen, da die Anfrage sehr umständlich wird. Wenn sie jedoch dominant in Ihrer Datenbank ist, sollten Sie darüber nachdenken, solche Methoden anzuwenden.

Lassen Sie uns mit ein paar einfachen Aussagen beginnen:

  • Einen und dieselben Datensatz aus der Datenbank sollte man besser nur einmal lesen.
  • Datensätze aus der Datenbank lesen sich effizienter "in Paketen", als einzeln.

Jetzt versuchen wir, die benötigte Abfrage zu konstruieren.

Schritt 1

Offensichtlich müssen wir bei der Initialisierung der Rekursion (wie könnte es anders sein!) die Aufzeichnungen der Blätter anhand der ursprünglichen Identifikatoren einlesen:

WITH RECURSIVE tree AS (
  SELECT
    rec -- dies ist der vollständige Datensatz der Tabelle
  , id::text chld -- dies ist der "Satz" der ursprünglichen Blätter, die hierher geführt haben
  FROM
    hier rec
  WHERE
    id = ANY('{1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192}'::integer[])
UNION ALL
  ...

Falls jemand es seltsam fand, dass ein „Set“ als String und nicht als Array gespeichert wird, gibt es dafür eine einfache Erklärung. Für Strings gibt es eine integrierte aggregierende „Verkettungs“-Funktion string_agg, für Arrays jedoch nicht. Auch wenn sie nicht schwer selbst zu implementieren ist..

Schritt 2

Nun möchten wir die IDs der Abschnitte ermitteln, die wir weiter bearbeiten müssen. Fast immer werden sie bei verschiedenen Einträgen des Ausgangssets dupliziert – daher sollten wir sie gruppieren, während wir die Informationen über die Quellblätter beibehalten.

Aber hier erwarten uns drei unangenehme Überraschungen:

  1. Der „unterrekursive“ Teil der Abfrage kann keine Aggregatfunktionen enthalten. GROUP BY.
  2. Der Zugriff auf die rekursive „Tabelle“ darf sich nicht in einer verschachtelten Unterabfrage befinden.
  3. Die Abfrage im rekursiven Teil darf keine CTE enthalten.

Glücklicherweise lassen sich all diese Probleme recht einfach umgehen. Fangen wir am Ende an.

CTEs im rekursiven Teil

So nicht funktioniert:

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

Und so funktioniert es – die Klammern regeln das!

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

Eine verschachtelte Abfrage zur rekursiven „Tabelle“

Hmm... Ein Verweis auf eine rekursive CTE kann nicht in einer verschachtelten Abfrage sein. Aber es kann innerhalb einer CTE sein! Und eine verschachtelte Abfrage kann bereits auf diese CTE zugreifen!

GROUP BY innerhalb der Rekursion

Das ist unangenehm, aber... Es gibt einen einfachen Weg, wie man GROUP BY mit Hilfe von DISTINCT ON und Fensterfunktionen simulieren kann!

SELECT
  (rec).pid id
, string_agg(chld::text, ',') chld
FROM
  tree
WHERE
  (rec).pid IS NOT NULL
GROUP BY 1 -- funktioniert nicht!

So funktioniert es allerdings!

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

Jetzt sehen wir, warum die numerische ID in Text umgewandelt wurde — damit sie durch Kommas verbunden werden konnten!

Schritt 3

Für das Finale bleibt uns nur noch wenig übrig:

  • Wir lesen die "Abschnitte" nach einer Gruppe von IDs aus
  • und ordnen die ausgelesenen Abschnitte den "Sätzen" der Ausgangsblätter zu
  • "entpacken" die Zeilen-Sätze mit Hilfe von unnest(string_to_array(chld, ',')::integer[])

MIT REKURSIVEN Baum ALS (
  AUSWÄHLEN
    rec
  , id::text chld
  AUS
    hier rec
  WO
    id = ANY('{1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192}'::integer[])
VEREINIGEN ALLES
  (
    MIT prnt ALS (
      AUSWÄHLEN DISTINCT ON((rec).pid)
        (rec).pid id
      , string_agg(chld::text, ',') ÜBER(PARTITIONIEREN VON (rec).pid) chld
      AUS
        Baum
      WO
        (rec).pid IST NICHT NULL
    )
    , knoten ALS (
      AUSWÄHLEN
        rec
      AUS
        hier rec
      WO
        id = ANY(ARRAY(
          AUSWÄHLEN
            id
          AUS
            prnt
        ))
    )
    AUSWÄHLEN
      knoten.rec
    , prnt.chld
    AUS
      prnt
    JOIN
      knoten
        ON (knoten.rec).id = prnt.id
  )
)
AUSWÄHLEN
  unnest(string_to_array(chld, ',')::integer[]) leaf
, (rec).* 
AUS
  Baum;

PostgreSQL Antipatterns: wie tief geht der Kaninchenbau? Lassen Sie uns die Hierarchie durchgehen.
[sehen Sie sich explain.tensor.ru an]

Quelle: habr.com

Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen 🔥 Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster