PostgreSQL antipattern'id: «Igavik — ei ole piir!», vĂ”i Natuke rekursioonist

Rekurssioon — vĂ€ga vĂ”imas ja mugav mehhanism, kui seotud andmete kohta tehakse samu toiminguid "sĂŒgavale". Kuid kontrollimatu rekurssioon on paha, mis vĂ”ib viia kas lĂ”putu tĂ€itmiseni protsessi, vĂ”i (mis juhtub sagedamini) kuni kogu saadaval mĂ€luruumi "nĂ€ljatamiseni"..

PostgreSQL antipattern'id: «Igavik — ei ole piir!», vĂ”i Natuke rekursioonist
Andmebaasid töötavad selles osas samade pĂ”himĂ”tete kohaselt — "kĂ€sid kaevata, mina ka kaevan". Teie pĂ€ring ei pruugi mitte ainult aeglustada naaberprotsesse, pidevalt kasutades protsessori ressursse, vaid vĂ”ib ka "maha kukkuda" kogu andmebaas, "söödud" kogu saadaval mĂ€lu. SeetĂ”ttu kaitse lĂ”putu rekursiooni eest on arendaja kohustus.

PostgreSQL-is tekkis vÔimalus kasutada rekursiivseid pÀringuid lÀbi WITH RECURSIVE juba ammuses versioonis 8.4, kuid siiani vÔib regulaarselt kohata potentsiaalselt haavatavaid "kaitsetuid" pÀringuid. Kuidas end sellistest probleemidest vabaks saada?

Ärge kirjutage rekursiivseid pĂ€ringuid

KĂŒll aga kirjutage rekursiivseid. Lugupidamisega, Teie K.O.

Tegelikult pakub PostgreSQL piisavalt palju funktsioone, millega saab ei rakendada rekursiooni.

Kasutage pĂ”himĂ”tteliselt teistsugust lĂ€henemist ĂŒlesandele

MĂ”nikord saab ĂŒlesandele lihtsalt vaadata 'teise nurga alt'. Sellise olukorra nĂ€idet tĂ”in ma artiklis «SQL HowTo: 1000 ja ĂŒks viis aggregeerimiseks» — rea numbrite korrutamine ilma kasutaja mÀÀratud aggregeerimisfunktsioonide rakendamiseta:

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 -- lÔpp-tulemuse valik
  i DESC
LIMIT 1;

Sellist pÀringut saab asendada matemaatikagurude variandiga:

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

Kasutage generate_series asemel silmusid

Oletame, et meie ees on ĂŒlesanne genereerida kĂ”ik vĂ”imalikud prefiksid stringile '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;

Kas siinkohal on tÔepoolest vajalik rekursioon?.. Kui kasutada LATERAL ja generate_series, siis ei ole isegi CTE-d vajalikud:

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

Muuda andmebaasi struktuuri

NÀiteks, teil on foorumi sÔnumite tabel, kus on seosed, kes kellele vastas, vÔi teema sotsiaalmeedias:

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

PostgreSQL antipattern'id: «Igavik — ei ole piir!», vĂ”i Natuke rekursioonist
Ja tĂŒĂŒpiline pĂ€ring, mis laadib kĂ”iki sĂ”numeid ĂŒhe teema kaupa, nĂ€eb vĂ€lja umbes selline:

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;

Kuna meil on alati vaja kogu teemat juure sÔnumist, miks siis mitte lisada selle identifikaator igasse kirjesse automaatsete vahenditega?

-- lisame vĂ€lja ĂŒhise teema identifikaatoriga ja indeksi sellele
ALTER TABLE message
  ADD COLUMN theme_id uuid;
CREATE INDEX ON message(theme_id);

-- initialize teema identifikaatori triggeris sisestamisel
CREATE OR REPLACE FUNCTION ins() RETURNS TRIGGER AS $$
BEGIN
  NEW.theme_id = CASE
    WHEN NEW.reply_to IS NULL THEN NEW.message_id -- vĂ”tame algsest sĂŒndmusest
    ELSE ( -- vÔi sÔnumist, millele vastame
      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 antipattern'id: «Igavik — ei ole piir!», vĂ”i Natuke rekursioonist
NĂŒĂŒd saab kogu meie rekursiivne pĂ€ring kokku tĂ”mmata vaid selliseks:

SELECT
  *
FROM
  message
WHERE
  theme_id = $1;

Kasutada rakenduslikke "piire"

Kui me mingil pÔhjusel ei saa andmebaasi struktuuri muuta, vaatame, millele toetuda, et isegi andmetes oleva vea korral ei tekiks lÔpmatu rekurss.

Rekursiooni 'sĂŒgavuse' loendur

Lihtsalt suurendame loendurit ĂŒhe vĂ”rra igal rekurssioni sammul kuni hetkeni, mil saavutame piiri, mida peame tĂ€iesti ebapiisavaks:

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

Pro: TsĂŒklilisuse katsetamisel teeme me ikkagi mitte rohkem kui mÀÀratud iteratsioonide 'sĂŒgavuse' piiri.
Contra: Ei ole garantiid, et me ei töötle uuesti sama kirjet — nĂ€iteks sĂŒgavusel 15 ja 25, ning edasi iga +10 sĂŒgavuse kohta. Ja 'laiuse' kohta ei ole keegi midagi lubanud.

Formaalsetel alustel ei saa see rekurss olema lÔpmatu, kuid kui igal sammul rekordite arv suurendab eksponentsiaalselt, teame kÔik, kuidas see lÔpeb...

PostgreSQL antipattern'id: «Igavik — ei ole piir!», vĂ”i Natuke rekursioonistvt 'KĂŒsimus malelaudade teradest'

Teekonna hoidja

Kirjutame jÀrjestikku kÔik kohtunud rekursiooni kÀigus objektide identifikaatorid massti, mis on ainulaadne 'tee' kuni sinna:

REKURSIOON T NAGU (
  VALIGE
    ARRAY[id] tee
  ...
ÜHENDAGE KÕIK
  VALIGE
    tee || id
  ...
  KUS
    id  ALL(T.tee) -- ei ĂŒhti ĂŒhegi
)

Pro: Kui andmetes on tsĂŒkkel, ei töötle me kindlasti sama kirjet ĂŒhe ja sama tee raames uuesti.
Contra: Kuid samas saame kÀia lÀbi, tÔeliselt, kÔik kirjed, nii et me ei korduks.

PostgreSQL antipattern'id: «Igavik — ei ole piir!», vĂ”i Natuke rekursioonistvt „Ratsu liikumise probleem“

Teepikkuse piirang

Kuna vĂ€ltida olukorda, kus rekursioon rĂ€ndab arusaamatusse sĂŒgavusse, vĂ”ime kahe eelneva meetodi kombineerida. VĂ”i, kui me ei soovi liialdavaid vĂ€lju toetada, tĂ€iendada rekursiooni jĂ€tkamise tingimist teepikkuse hindamisega:

REKURSIOON T NAGU (
  VALIGE
    ARRAY[id] tee
  ...
ÜHENDAGE KÕIK
  VALIGE
    tee || id
  ...
  KUS
    id  ALL(T.tee) JA
    array_length(T.tee, 1) < 10
)

Valige endale sobiv meetod!

Allikas: habr.com

Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster