PostgreSQL Antipatterns: överföring av uppsÀttningar och urval i SQL

Utvecklare har ibland behov av att överföra en uppsÀttning parametrar eller till och med en hel urval till "indatan". Ibland stöter man pÄ mycket mÀrkliga lösningar pÄ detta problem.
PostgreSQL Antipatterns: överföring av uppsÀttningar och urval i SQL
LÄt oss gÄ "bakifrÄn" och titta pÄ hur man inte bör göra, varför, och hur man kan göra det bÀttre.

Direkt "injicering" av vÀrden i begÀran

Ser oftast ut ungefÀr sÄ hÀr:

query = "SELECT * FROM tbl WHERE id = " + value

... eller sÄ hÀr:

query = "SELECT * FROM tbl WHERE id = :param".format(param=value)

Det finns redan mycket skrivet, sagt och till och med ritat om detta:

PostgreSQL Antipatterns: överföring av uppsÀttningar och urval i SQL

NÀstan alltid Àr detta en direkt vÀg till SQL-injektioner och onödig belastning pÄ affÀrslogiken, som tvingas "klistra" ihop din begÀrans strÀng.

En del av denna metod kan vara berÀttigad endast om det Àr nödvÀndigt att anvÀnda partitionering i versioner av PostgreSQL 10 och lÀgre för att fÄ en mer effektiv plan. I dessa versioner bestÀms listan över skannade avsnitt utan att ta hÀnsyn till de överförda parametrarna, endast baserat pÄ begÀrans kropp.

$n-argument

AnvĂ€ndning platsbehĂ„llare parametrar – det Ă€r bra, eftersom det tillĂ„ter anvĂ€ndning av PREPARED STATEMENTS, vilket minskar belastningen bĂ„de pĂ„ affĂ€rslogiken (begĂ€rans strĂ€ng skapas och överförs endast en gĂ„ng) och pĂ„ databasservern (ingen behov av omparsing och planering för varje begĂ€rans instans).

Variabelt antal argument

Problem kommer att vÀnta pÄ oss nÀr vi vill överföra ett okÀnt antal argument:

... id IN ($1, $2, $3, ...) -- $1 : 2, $2 : 3, $3 : 5, ...

Om vi lÀmnar begÀran pÄ detta sÀtt, kommer detta, Àven om det skyddar oss mot potentiella injektioner, ÀndÄ att leda till behovet av att klistra ihop/parse begÀran för varje alternativ beroende pÄ antalet argument.Det Àr redan bÀttre Àn att göra detta varje gÄng, men vi kan klara oss utan det.

Det rÀcker med att överföra en enda parameter som innehÄller en serialiserad representation av en array:

... id = ANY($1::integer[]) -- $1 : '{2,3,5,8,13}'

Den enda skillnaden Àr nödvÀndigheten att uttryckligen konvertera argumentet till den nödvÀndiga arraytypen. Men detta orsakar inga problem eftersom vi redan vet var vi riktar oss.

Överföring av urval (matris)

Detta Àr vanligtvis olika alternativ för att överföra datamÀngder för insÀttning i databasen "i ett enda anrop":

INSERT INTO tbl(k, v) VALUES($1,$2),($3,$4),...

Förutom de problem som beskrivits ovan med "Ă„terklistring" av begĂ€ran kan detta dessutom leda till out of memory och serverfall. Anledningen Ă€r enkel – genom argumenten reserverar PG extra minne, och antalet poster i uppsĂ€ttningen begrĂ€nsas endast av de praktiska kraven i affĂ€rslogiken. I sĂ€rskilt kliniska fall har man sett »numeriska» argument över $9000 – det behövs inte sĂ„.

LÄt oss skriva om frÄgan och anvÀnda »tvÄ-nivÄ» serialisering:

INSERT INTO tbl
SELECT
  unnest[1]::text k
, unnest[2]::integer v
FROM (
  SELECT
    unnest($1::text[])::text[] -- $1 : '{"{a,1}","{b,2}","{c,3}","{d,4}"}'
) T;

Ja, i fallet med »komplexa» vÀrden inuti arrayen, mÄste de omges av citattecken.
Det Àr tydligt att pÄ detta sÀtt kan man »utveckla» urvalet med ett godtyckligt antal fÀlt.

unnest, unnest, 


Ibland dyker det upp alternativ att istÀllet för »array av arrays» anvÀnda flera »kolumn-arrays» som jag nÀmnde i den föregÄende artikeln:

SELECT
  unnest($1::text[]) k
, unnest($2::integer[]) v;

Med denna metod, om man gör ett misstag vid generering av vÀrdelistor för olika kolumner, Àr det vÀldigt enkelt att fÄ helt oförutsedda resultat, som dessutom beror pÄ serverversion:

-- $1 : '{a,b,c}', $2 : '{1,2}'
-- PostgreSQL 9.4
k | v
-----
a | 1
b | 2
c | 1
a | 2
b | 1
c | 2
-- PostgreSQL 11
k | v
-----
a | 1
b | 2
c |

JSON

Redan frÄn version 9.3 introducerade PostgreSQL fullstÀndiga funktioner för att arbeta med json-typ. DÀrför, om definieringen av inparametrarna sker i webblÀsaren, kan du direkt dÀr och dÄ skapa json-objektet för SQL-frÄgan:

SELECT
  key k
, value v
FROM
  json_each($1::json); -- '{"a":1,"b":2,"c":3,"d":4}'

För tidigare versioner kan samma metod anvÀndas för each(hstore), men en korrekt »svÀngning» med inneslutning av komplexa objekt i hstore kan orsaka problem.

json_populate_recordset

Om du i förvÀg vet att data frÄn »ingÄngs» json-arrayen ska fylla en viss tabell, kan du kraftigt spara pÄ »avdereferering» av fÀlt och konvertering till nödvÀndiga typer, genom att anvÀnda funktionen json_populate_recordset:

SELECT
  *
FROM
  json_populate_recordset(
    NULL::pg_class
  , $1::json -- $1 : '[{"relname":"pg_class","oid":1262},{"relname":"pg_namespace","oid":2615}]'
  );

json_to_recordset

Och denna funktion kommer helt enkelt att »utveckla» den överförda arrayen av objekt till urvalet, utan att förlita sig pÄ tabellformatet:

SELECT
  *
FROM
  json_to_recordset($1::json) T(k text, v integer);
-- $1 : '[{"k":"a","v":1},{"k":"b","v":2}]'
k | v
-----
a | 1
b | 2

TEMPORARY TABLE

Men om datavolymen i den överförda uppsÀttningen Àr mycket stor, Àr det svÄrt, och ibland omöjligt, att skicka det som en enda serialiserad parameter, vilket krÀver en engÄngs tilldelning av stora mÀngder minneTill exempel, om du behöver samla in en stor mÀngd data om hÀndelser frÄn ett externt system under en lÀngre tid och sedan vill behandla den en gÄng pÄ databassidan.

I det hÀr fallet Àr den bÀsta lösningen att anvÀnda temporÀra tabeller:

CREATE TEMPORARY TABLE tbl(k text, v integer);
...
INSERT INTO tbl(k, v) VALUES($1, $2); -- upprepa mÄnga gÄnger
...
-- hÀr gör vi nÄgot anvÀndbart med hela den hÀr tabellen

Denna metod Àr sÀrskilt bra för sÀllan överföring av stora volymer av data.
FrĂ„n perspektivet av att beskriva strukturen av dina data, skiljer sig en temporĂ€r tabell frĂ„n en "vanlig" enbart genom ett enda kĂ€nnetecken i systemtabellen pg_class, och i pg_type, pg_depend, pg_attribute, pg_attrdef, 
 — i sjĂ€lva verket inte alls.

DÀrför, i webbsystem med ett stort antal kortlivade anslutningar, kommer en sÄdan tabell att generera nya systeminlÀgg varje gÄng, vilka tas bort vid stÀngning av anslutningen till databasen. Som ett resultat, okontrollerad anvÀndning av TEMP TABLE leder till "uppsvÀllning" av tabeller i pg_catalog och sÀnker prestandan av mÄnga operationer som anvÀnder dem.
SjÀlvklart kan detta hanteras med hjÀlp av periodisk VACUUM FULL genom systemkatalogtabeller.

Sessionvariabler

Anta att databehandlingen frÄn det föregÄende fallet Àr tillrÀckligt komplicerad för att rymmas inom en enda SQL-frÄga, men att vi vill göra det ganska ofta. Det vill sÀga vi vill anvÀnda procedurbehandling i DO-blocket, men att anvÀnda datatransfer via temporÀra tabeller skulle bli för kostsamt.

Vi kan inte heller anvÀnda $n-parametrar för att överföra till det anonyma blocket. Lösningen ligger i sessionvariabler och funktionen current_setting.

Före version 9.2 var det nödvÀndigt att först konfiguera ett speciellt namnutrymme custom_variable_classes för "egna" sessionvariabler. I aktuella versioner kan man skriva ungefÀr sÄ hÀr:

SET my.val = '{1,2,3}';
DO $$
DECLARE
  id integer;
BEGIN
  FOR id IN (SELECT unnest(current_setting('my.val')::integer[])) LOOP
    RAISE NOTICE 'id : %', id;
  END LOOP;
END;
$$ LANGUAGE plpgsql;
-- NOTICE:  id : 1
-- NOTICE:  id : 2
-- NOTICE:  id : 3

I andra stödda procedurprogramsprÄk kan man hitta andra lösningar.

KÀnner du till nÄgra andra sÀtt? Dela med dig i kommentarerna!

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