PostgreSQL Antipatterns: «OÀndlighet Àr inte grÀnsen!», eller Lite om rekursion

Rekursion — en mycket kraftfull och bekvĂ€m mekanism nĂ€r samma Ă„tgĂ€rder utförs 'djupt' pĂ„ relaterade data. Men okontrollerad rekursion Ă€r en ondska som kan leda till antingen oĂ€ndlig körning av processen, eller (vilket sker oftare) till att 'Ă€ta upp' all tillgĂ€nglig minne.

PostgreSQL Antipatterns: «OÀndlighet Àr inte grÀnsen!», eller Lite om rekursion
Databaser fungerar pĂ„ samma sĂ€tt — "sa de att grĂ€va, sĂ„ grĂ€ver jag". Din frĂ„ga kan inte bara bromsa angrĂ€nsande processer genom att stĂ€ndigt ta CPU-resurser, utan ocksĂ„ 'falla' hela databasen genom att 'Ă€ta' all tillgĂ€nglig minne. DĂ€rför skydd mot oĂ€ndlig rekursion Ă€r utvecklarens ansvar.

I PostgreSQL fanns möjligheten att anvÀnda rekursiva frÄgor via WITH RECURSIVE sedan urminnes tider i version 8.4, men man kan fortfarande regelbundet stöta pÄ potentiellt sÄrbara 'oskyddade' frÄgor. Hur undviker man sÄdana problem?

Att inte skriva rekursiva frÄgor

Utan att skriva icke-rekursiva. Med vÀnliga hÀlsningar, Er K.O.

I sjÀlva verket erbjuder PostgreSQL en tillrÀckligt stor mÀngd funktionalitet som man kan utnyttja för att inte anvÀnda rekursion.

AnvÀnda en principielt annan metod för uppgiften

Ibland kan man helt enkelt betrakta uppgiften 'frĂ„n en annan sida'. Ett exempel pĂ„ en sĂ„dan situation presenterade jag i artikeln «SQL HowTo: 1000 och ett sĂ€tt att aggregera» — multiplikation av en uppsĂ€ttning tal utan att anvĂ€nda anvĂ€ndardefinierade aggregatfunktioner:

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 -- val av det slutgiltiga resultatet
  i DESC
LIMIT 1;

En sÄdan frÄga kan ersÀttas med en version av matematikexperter:

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

AnvÀnd generate_series istÀllet för loopar

Anta att vi har uppgiften att generera alla möjliga prefix för strÀngen '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;

Behövs verkligen rekursion hÀr?.. Om man utnyttjar LATERAL och generate_series, sÄ behövs inte ens CTE:

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

Ändra databasstrukturen

Till exempel, om du har en forum meddelandetabell och kopplingar för vem som svarade pÄ vem eller ett trÄd i sociala medier:

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

PostgreSQL Antipatterns: «OÀndlighet Àr inte grÀnsen!», eller Lite om rekursion
Typiskt sett ser en frÄga för att hÀmta alla meddelanden i ett Àmne ut ungefÀr sÄ hÀr:

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;

Men eftersom vi alltid behöver hela trÄden frÄn meddelandet, varför inte lÀgga till dess identifierare i varje post automatiskt?

-- lÄt oss lÀgga till ett fÀlt med ett gemensamt Àmnes-ID och en index för det
ALTER TABLE message
  ADD COLUMN theme_id uuid;
CREATE INDEX ON message(theme_id);

-- initialisera Àmnes-ID i triggern vid insÀttning
CREATE OR REPLACE FUNCTION ins() RETURNS TRIGGER AS $$
BEGIN
  NEW.theme_id = CASE
    WHEN NEW.reply_to IS NULL THEN NEW.message_id -- ta frÄn startmeddelandet
    ELSE ( -- eller frÄn meddelandet som vi svarar pÄ
      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: «OÀndlighet Àr inte grÀnsen!», eller Lite om rekursion
Nu kan hela vÄr rekursiva frÄga reduceras till nÄgot sÄ enkelt som följande:

SELECT
  *
FROM
  message
WHERE
  theme_id = $1;

AnvÀnda applikations "begrÀnsningar"

Om vi av nÄgon anledning inte kan Àndra databasstrukturen, lÄt oss titta pÄ vad vi kan luta oss mot sÄ att Àven nÀr det finns fel i data, inte leder till oÀndlig rekursion.

RÀknare för "djupet" av rekursion

Vi ökar helt enkelt rÀknaren med ett pÄ varje steg av rekursionen fram till den punkt vi anser vara uppenbart otillÄten:

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

Pro: Vid försök till oÀndlighet kommer vi ÀndÄ inte att göra mer Àn det angivna antalet iterationer "ner".
Contra: Det finns ingen garanti för att vi inte kommer att behandla samma post flera gÄnger - till exempel pÄ djupet 15 och 25, och sedan vidare för varje +10. Ingen har lovat nÄgot "pÄ bredden" heller.

Formellt sett kommer sĂ„dan rekursion inte att vara oĂ€ndlig, men om antalet poster ökar exponentiellt vid varje steg, vet vi alla hur det slutar


PostgreSQL Antipatterns: «OÀndlighet Àr inte grÀnsen!», eller Lite om rekursionse "Problemet med korn pÄ schackbrÀdet"

VÀktare av "vÀgen"

Vi skriver sekventiellt in alla identifierare vi stöter pÄ under rekursionsvÀgen i en array som Àr den unika "vÀgen" fram till den:

MED REKURSIV T SOM (
  VÄLJ
    ARRAY[id] vÀg
  ...
UNION ALL
  VÄLJ
    vÀg || id
  ...
  DÄR
    id  ALL(T.vÀg) -- matchar inte nÄgon av
)

Pro: Vid förekomst av en cykel i datan kommer vi absolut inte att bearbeta samma post igen inom samma vÀg.
Contra: Men vi kan bokstavligen gÄ igenom alla poster utan att upprepa oss.

PostgreSQL Antipatterns: «OÀndlighet Àr inte grÀnsen!», eller Lite om rekursionse "SchackhÀstens problem"

BegrÀnsning av vÀgslÀngd

För att undvika situationen med "vilsen" rekursion pÄ en otydlig djup kan vi kombinera de tvÄ föregÄende metoderna. Eller, om vi inte vill stödja onödiga fÀlt, kan vi komplettera villkoret för fortsatt rekursion med en utvÀrdering av vÀgslÀngden:

MED REKURSIV T SOM (
  VÄLJ
    ARRAY[id] vÀg
  ...
UNION ALL
  VÄLJ
    vÀg || id
  ...
  DÄR
    id  ALL(T.vÀg) OCH
    array_length(T.vÀg, 1) < 10
)

VĂ€lj metod efter eget tycke!

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster