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

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 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 â 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 :
CREATE TABLE message(
message_id
uuid
PRIMARY KEY
, reply_to
uuid
REFERENCES message
, body
text
);
CREATE INDEX ON message(reply_to); 
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(); 
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âŠ
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.
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
