PostgreSQL Antipatterns: "A végtelen nem határ!", avagy egy kis a rekurzióról

Rekurzió — egy nagyon hatékony és kényelmes mechanizmus, ha az összefüggő adatokon ugyanazokat a műveleteket hajtják végre "mélyen". De a kontrollálatlan rekurzió a gonosz, ami akár végtelen végrehajtást is eredményezhet, vagy (ami gyakrabban előfordul) a „teljesen elnyeli” az összes elérhető memóriát.

PostgreSQL Antipatterns: "A végtelen nem határ!", avagy egy kis a rekurzióról
A DBMS-ek ebben a tekintetben hasonló elveken működnek — "ha azt mondták, hogy ásson, én is ások". A lekérdezésed nemcsak lelassíthatja a szomszédos folyamatokat, folyamatosan elfoglalva a CPU erőforrásait, hanem az egész adatbázist is „lerombolhatja”, elnyelve az összes elérhető memóriát. Ezért a végtelen rekurzió elleni védelem a fejlesztő kötelessége.

A PostgreSQL-ben a rekurzív lekérdezések használatának lehetősége WITH RECURSIVE már a régi 8.4-es verzióban is megjelent, de még mindig rendszeresen találkozni lehet potenciálisan sebezhető "védtelen" lekérdezésekkel. Hogyan lehet megszabadulni az ilyen problémáktól?

Ne írj rekurzív lekérdezéseket

Hanem írj nem rekurzívakat. Tisztelettel, Ön K.O.

Valójában a PostgreSQL elegendő funkciót biztosít, amelyeket felhasználhatsz ahhoz, hogy nem alkalmazza a rekurziót.

Alapvetően más megközelítést alkalmazz a feladathoz

Néha csak más szemszögből kell megvizsgálni a feladatot. Egy ilyen helyzet példáját a „SQL HowTo: 1000 és egy mód az aggregálásra” című cikkemben ismertettem — a számok halmazának szorzása felhasználói aggregáló függvények nélkül:

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 -- az utolsó eredmény kiválasztása
  i DESC
LIMIT 1;

Ezt a lekérdezést helyettesítheted a matematikai szakértők verziójával:

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

Használj generate_series-t ciklusok helyett

Tegyük fel, hogy az a feladatunk, hogy generáljuk az összes lehetséges előtagját a szónak 'abcdefgh':

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

Tényleg szükség van itt rekurzióra?.. Ha helyette alkalmazod a LATERAL és generate_series-t, akkor még CTE-re sincs szükség:

SELECT
  substr(str, 1, ln) str
FROM
  (VALUES('abcdefgh')) T(str)
, LATERAL(
    SELECT generate_series(length(str), 1, -1) ln
  ) X;

Módosítsd az adatbázis szerkezetét

Például van egy fórumbeszélgetések táblád, amely a válaszadás kapcsolatát mutatja, vagy a szálat szociális hálózaton:

CREATE TABLE message(
  message_id
    uuid
      PRIMARY KEY
, reply_to
    uuid
      REFERENCES message
, body
    text
);
CREATE INDEX ON message(reply_to);

PostgreSQL Antipatterns: "A végtelen nem határ!", avagy egy kis a rekurzióról
A tipikus lekérdezés, amely betölti az összes üzenetet egy témában, körülbelül így néz ki:

WITH RECURSIVE T AS (
  SELECT
    *
  FROM
    message
  WHERE
    message_id = $1
UNION ALL
  SELECT
    m.*
  FROM
    T
  JOIN
    message m
      ON m.reply_to = T.message_id
)
TABLE T;

De mivel mindig szükségünk van az egész témára a gyökérüzenettől, miért ne tennénk hozzá az azonosítóját minden egyes bejegyzéshez automatikusan?

-- adjunk hozzá egy mezőt a téma közös azonosítójával és indexet rá
ALTER TABLE message
  ADD COLUMN theme_id uuid;
CREATE INDEX ON message(theme_id);

-- inicializáljuk a téma azonosítóját egy triggerrel beszúráskor
CREATE OR REPLACE FUNCTION ins() RETURNS TRIGGER AS $$
BEGIN
  NEW.theme_id = CASE
    WHEN NEW.reply_to IS NULL THEN NEW.message_id -- vegyük az indító eseményből
    ELSE ( -- vagy a válaszolt üzenetből
      SELECT
        theme_id
      FROM
        message
      WHERE
        message_id = NEW.reply_to
    )
  END;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER ins BEFORE INSERT
  ON message
    FOR EACH ROW
      EXECUTE PROCEDURE ins();

PostgreSQL Antipatterns: "A végtelen nem határ!", avagy egy kis a rekurzióról
Most a teljes rekurzív lekérdezésünk így egyszerűsödhet:

SELECT
  *
FROM
  message
WHERE
  theme_id = $1;

Használjunk alkalmazási „határokat”

Ha valamilyen okból nem tudjuk megváltoztatni az adatbázis struktúráját, nézzük meg, mire támaszkodhatunk, hogy még a hibás adatok megléte se vezessen végtelen rekurzióhoz.

A rekurzió „mélység” számlálója

Egyszerűen minden rekurziós lépésnél növeljük a számlálót egy egységgel, amíg el nem érjük a sokat sürgetett határt, amelyet nyilvánvalóan nem megfelelőnek tartunk:

WITH RECURSIVE T AS (
  SELECT
    0 i
  ...
UNION ALL
  SELECT
    i + 1
  ...
  WHERE
    T.i < 64 -- a határ
)

Pro: Ciklusba lépés kísérletekor sem végzünk többet az előírt iterációk számánál „mélységben”.
Contra: Nincs garancia arra, hogy nem dolgozunk fel ismét egy ugyanazon bejegyzést — például 15-ös és 25-ös mélységnél, na és aztán minden +10-nél. De a „szélességre” senki sem ígért semmit.

Formailag ez a rekurzió nem lesz végtelen, de ha minden lépésnél exponenciálisan növekszik a bejegyzések száma, mindannyian jól tudjuk, hogy ez mivel végződik…

PostgreSQL Antipatterns: "A végtelen nem határ!", avagy egy kis a rekurzióróllásd: „A sakkmezőkről” szóló feladat

A „út” őrzője

Fokozatosan felírjuk mindazokat az objektumazonosítókat, amelyekkel a rekurzió során találkozunk, egy tömbbe, amely egyedi „út” hozzánk:

WITH RECURSIVE T AS (
  SELECT
    ARRAY[id] path
  ...
UNION ALL
  SELECT
    path || id
  ...
  WHERE
    id  ALL(T.path) -- nem egyezik egyikkel sem
)

Pro: Ha a dátumban ciklus van, akkor biztosan nem fogunk ugyanazt a bejegyzést újra feldolgozni egy adott útvonalon belül.
Contra: De közben szó szerint bejárhatunk minden bejegyzést, anélkül, hogy megismételnénk őket.

PostgreSQL Antipatterns: "A végtelen nem határ!", avagy egy kis a rekurzióróllásd: "A ló lépései problémája"

Útvonalhossz-korlátozás

Az "elveszett" rekurzió elkerülése érdekében, amely érthetetlen mélységben jár, kombinálhatjuk a két előző módszert. Vagy ha nem szeretnénk felesleges mezőket fenntartani, kiegészíthetjük a rekurzió folytatásának feltételét az útvonal hosszának értékelésével:

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
)

Válaszd ki a számodra tetsző módot!

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster