PostgreSQL Antipatterns: „Unendlichkeit ist kein Limit!“, oder Ein wenig ĂŒber Rekursion

Rekursion — ein Ă€ußerst leistungsfĂ€higer und praktischer Mechanismus, wenn die gleichen Aktionen bei miteinander verbundenen Daten „nach unten“ ausgefĂŒhrt werden. Aber unkontrollierte Rekursion ist eine Gefahr, die zu endlosen AusfĂŒhrungen des Prozesses fĂŒhren kann oder (was hĂ€ufiger vorkommt) zu dem „Verschlingen“ des gesamten verfĂŒgbaren Speichers..

PostgreSQL Antipatterns: „Unendlichkeit ist kein Limit!“, oder Ein wenig ĂŒber Rekursion
Datenbanksysteme arbeiten in dieser Hinsicht nach denselben Prinzipien — „Sie sagten, graben Sie, also grabe ich.“. Ihre Abfrage kann nicht nur benachbarte Prozesse verlangsamen, indem sie stĂ€ndig CPU-Ressourcen verbraucht, sondern auch die gesamte Datenbank „zum Absturz bringen“, indem sie den gesamten verfĂŒgbaren Speicher „auffrisst“. Daher ist der Schutz vor endloser Rekursion die Verantwortung des Entwicklers selbst.

In PostgreSQL besteht die Möglichkeit, rekursive Abfragen ĂŒber WITH RECURSIVE schon seit den „guten alten Zeiten“ der Version 8.4, aber bis heute können regelmĂ€ĂŸig potenziell anfĂ€llige „schutzlose“ Abfragen festgestellt werden. Wie können Sie sich von solchen Problemen befreien?

Schreiben Sie keine rekursiven Abfragen.

Sondern schreiben Sie nicht-rekursiven. Mit Respekt, Ihr K.O.

TatsÀchlich bietet PostgreSQL eine Vielzahl an Funktionen, die man nutzen kann, um nicht Rekursion anzuwenden.

Einen grundlegend anderen Ansatz fĂŒr das Problem verwenden

Manchmal kann man die Aufgabe auch einfach aus einer anderen Perspektive betrachten. Ein Beispiel fĂŒr eine solche Situation habe ich in dem Artikel „SQL HowTo: 1000 und ein Weg der Aggregation“ gebracht – die Multiplikation einer Anzahl von Zahlen ohne benutzerdefinierte Aggregatfunktionen:

WITH RECURSIVE src AS (
  SELECT '{2,3,5,7,11,13,17,19}'::integer[] arr
)
, T(i, val) AS (
  SELECT
    1::bigint
  , 1
UNION ALL
  SELECT
    i + 1
  , val * arr[i]
  FROM
    T
  , src
  WHERE
    i <= array_length(arr, 1)
)
SELECT
  val
FROM
  T
ORDER BY -- Auswahl des finalen Ergebnisses
  i DESC
LIMIT 1;

Eine solche Abfrage kann durch die Methode von Mathematikern ersetzt werden:

WITH src AS (
  SELECT unnest('{2,3,5,7,11,13,17,19}'::integer[]) prime
)
SELECT
  exp(sum(ln(prime)))::integer val
FROM
  src;

generate_series anstelle von Schleifen verwenden

Angenommen, wir stehen vor der Aufgabe, alle möglichen PrĂ€fixe fĂŒr die Zeichenfolge 'abcdefgh':

zu generieren:
WITH RECURSIVE T AS (
  SELECT 'abcdefgh' str
UNION ALL
  SELECT
    substr(str, 1, length(str) - 1)
  FROM
    T
  WHERE
    length(str) > 1
)
TABLE T;

Ist hier wirklich Rekursion nötig?.. Wenn wir LATERAL und generate_series, verwenden, dann sind nicht einmal CTE erforderlich:

WÄHLEN
  substr(str, 1, ln) str
VON
  (WERT('abcdefgh')) T(str)
, LATERAL(
    WÄHLEN generate_series(lĂ€nge(str), 1, -1) ln
  ) X;

Datenbankstruktur Àndern

Zum Beispiel haben Sie eine Tabelle mit ForenbeitrÀgen, die zeigt, wer wem geantwortet hat oder den Thread in einem sozialen Netzwerk:

ERSTELLEN TABELLE nachricht(
  nachricht_id
    uuid
      PRIMARY KEY
, antwort_auf
    uuid
      REFERENZEN nachricht
, text
    text
);
INDEX ERSTELLEN FÜR nachricht(antwort_auf);

PostgreSQL Antipatterns: „Unendlichkeit ist kein Limit!“, oder Ein wenig ĂŒber Rekursion
Ein typischer Abfrage zum Laden aller Nachrichten zu einem Thema sieht etwa so aus:

MIT REKURSIV T ALS (
  WÄHLEN
    *
  VON
    nachricht
  WO
    nachricht_id = $1
UNION ALLE
  WÄHLEN
    m.*
  VON
    T
  JOIN
    nachricht m
      ON m.antwort_auf = T.nachricht_id
)
TABELLE T;

Aber da wir immer das gesamte Thema vom Ausgangsnachricht benötigen, warum fĂŒgen wir nicht seine ID automatisch in jeden Datensatz ?

-- FĂŒgen wir ein Feld mit der gemeinsamen Themen-ID und einen Index darauf hinzu
ALTER TABLE nachricht
  FÜGE SPALTE theme_id uuid HINZU;
INDEX ERSTELLEN FÜR nachricht(theme_id);

-- initialisieren Sie die Themen-ID in einem Trigger bei der EinfĂŒgung
CREATE OR REPLACE FUNCTION ins() RETURNS TRIGGER AS $$
BEGIN
  NEW.theme_id = FALLS
    WENN NEW.antwort_auf IST NULL DANN NEW.nachricht_id -- nehmen Sie vom Startereignis
    SONST ( -- oder von der Nachricht, auf die wir antworten
      WÄHLEN
        theme_id
      VON
        nachricht
      WO
        nachricht_id = NEW.antwort_auf
    )
  END;
  GEBEN SIE NEU ZURÜCK;
END;
$$ SPRACHE plpgsql;

CREATE TRIGGER ins VOR INSERT
  AUF nachricht
    FÜR JEDEN DATENZEILE
      FÜHREN SIE DAS VERFAHREN ins AUS;

PostgreSQL Antipatterns: „Unendlichkeit ist kein Limit!“, oder Ein wenig ĂŒber Rekursion
Jetzt kann unsere rekursive Abfrage auf folgendes reduziert werden:

SELECT
  *
FROM
  message
WHERE
  theme_id = $1;

Anwendungsbezogene "Grenzen" nutzen

Falls wir die Struktur der Datenbank aus bestimmten GrĂŒnden nicht Ă€ndern können, sollten wir ĂŒberlegen, auf welche Aspekte wir uns stĂŒtzen können, damit selbst ein Fehler in den Daten nicht zu einer endlosen Rekursion fĂŒhrt.

ZĂ€hler fĂŒr die Rekursionstiefe

Wir erhöhen einfach den ZÀhler bei jedem Schritt der Rekursion bis zu dem Punkt, den wir als eindeutig unangemessen erachten:

WITH RECURSIVE T AS (
  SELECT
    0 i
  ...
UNION ALL
  SELECT
    i + 1
  ...
  WHERE
    T.i < 64 -- Grenze
)

Pro: Bei einem Versuch zur Endlosschleife werden wir dennoch nicht mehr als die angegebene Anzahl an Iterationen "nach unten" durchfĂŒhren.
Contra: Es gibt keine Garantie, dass wir nicht einen gleichen Datensatz mehrfach verarbeiten – beispielsweise auf der Tiefe 15 und 25, und dann jeweils alle 10 tiefer. Auch zum "seitlichen" Zugriff wurde nichts versprochen.

Formal wird diese Rekursion nicht unendlich sein, aber wenn die Anzahl der DatensÀtze bei jedem Schritt exponentiell zunimmt, wissen wir alle, wie das endet


PostgreSQL Antipatterns: „Unendlichkeit ist kein Limit!“, oder Ein wenig ĂŒber RekursionSiehe "Die Aufgabe mit den Körnern auf dem Schachbrett"

WĂ€chter des "Pfades"

Wir fĂŒgen nacheinander alle Identifikatoren der Objekte, die wir auf dem Weg der Rekursion treffen, in ein Array ein, das den einzigartigen "Weg" zu ihnen darstellt:

WITH RECURSIVE T AS (
  SELECT
    ARRAY[id] path
  ...
UNION ALL
  SELECT
    path || id
  ...
  WHERE
    id  ALL(T.path) -- entspricht keinem der
)

Pro: Wenn ein Zyklus in den Daten vorhanden ist, werden wir auf keinen Fall den gleichen Datensatz im Rahmen eines einzigen Weges erneut verarbeiten.
Contra: Aber dabei können wir tatsÀchlich alle DatensÀtze durchlaufen, ohne uns zu wiederholen.

PostgreSQL Antipatterns: „Unendlichkeit ist kein Limit!“, oder Ein wenig ĂŒber RekursionSiehe "Das Schachproblem"

EinschrÀnkung der PfadlÀnge

Um die Situation des "Umherirrens" der Rekursion in unverstĂ€ndlicher Tiefe zu vermeiden, können wir die beiden vorherigen Methoden kombinieren. Oder, falls wir keine zusĂ€tzlichen Felder unterstĂŒtzen wollen, die Fortsetzung der Rekursion mit einer Bewertung der PfadlĂ€nge ergĂ€nzen:

WITH RECURSIVE T AS (
  SELECT
    ARRAY[id] path
  ...
UNION ALL
  SELECT
    path || id
  ...
  WHERE
    id  ALL(T.path) AND
    array_length(T.path, 1) < 10
)

WĂ€hlen Sie die Methode nach Ihrem Geschmack!

Quelle: habr.com

Kaufen Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server đŸ”„ Kaufen Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster