PostgreSQL Antipatterns: overførsel af sæt og valg i SQL

Udvikleren kan fra tid til anden have behov for at overføre et sæt parametre eller endda en hel sampling som input. Nogle gange støder man på meget mærkelige løsninger på denne opgave.
PostgreSQL Antipatterns: overførsel af sæt og valg i SQL
Lad os gå 'den modsatte vej' og se på, hvordan man ikke bør gøre det, hvorfor, og hvordan man kan gøre det bedre.

Direkte 'indsættelse' af værdier i forespørgslen

ser normalt omtrent sådan ud:

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

... eller sådan her:

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

Der er sagt, skrevet og endda tegnet ret mange om denne metode:

PostgreSQL Antipatterns: overførsel af sæt og valg i SQL

Næsten altid er det — en direkte vej til SQL-injektioner og unødvendig belastning af forretningslogikken, som er tvunget til at 'lime' din forespørgselsstreng.

Delvist kan denne tilgang være begrundet kun i tilfælde af behovet for at anvende partitionering i versioner af PostgreSQL 10 og tidligere for at opnå en mere effektiv plan. I disse versioner bestemmes listen over scannede sektioner endnu uden hensyntagen til de overførte parametre, kun baseret på forespørgselens krop.

$n-argumenter

Brugen af pladsholdere parametre — det er godt, det tillader brug af PREPARED STATEMENTS, hvilket reducerer belastningen både på forretningslogikken (forespørgselsstrengen dannes og overføres kun én gang) og på databaserver (der kræves ikke gentagen parsing og planlægning for hver instans af forespørgslen).

Variabelt antal argumenter

Problemer vil vente på os, når vi ønsker at overføre et på forhånd ukendt antal argumenter:

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

Hvis man lader forespørgslen stå i denne form, vil det, selvom det beskytter os mod potentielle injektioner, stadig føre til behovet for limning/parsing af forespørgslen for hver variant fra antallet af argumenter.Det er allerede bedre end at gøre dette hver gang, men man kan klare sig uden det.

Det er tilstrækkeligt at overføre bare ét parameter, der indeholder en serialiseret repræsentation af en array:

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

Den eneste forskel er nødvendigheden af at eksplicit konvertere argumentet til den ønskede array-type. Men det giver ikke problemer, da vi allerede ved, hvor vi skal hen.

Overførsel af oplysninger (matrix)

Normalt handler det om forskellige måder at overføre datasæt til indsættelse i databasen 'i én forespørgsel':

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

Udover de tidligere nævnte problemer med 'limning' af forespørgslen, kan dette også føre til out of memory og servernedbrud. Årsagen er simpel — under argumenterne reserverer PG ekstra hukommelse, og antallet af poster i sættet er kun begrænset af de funktionelle ønsker i forretningslogikken. I særligt kliniske tilfælde har man set «nummererede» argumenter over $9000 — det behøver man ikke.

Lad os omskrive forespørgslen ved at anvende «to-niveau» 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 tilfælde af «komplekse» værdier inden for arrayet skal de indrammes med citationstegn.
Det er tydeligt, at man ved denne metode kan «udvikle» et udtræk med et vilkårligt antal felter.

unnest, unnest, …

Der opstår lejlighedsvis varianter, hvor man i stedet for «array af arrays» sender flere «arrays af kolonner», som jeg har nævnt i den tidligere artikel:

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

Ved denne metode er det meget nemt at få helt uventede resultater, afhængigt af serverversionen:

-- $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

Siden version 9.3 har PostgreSQL haft fuld funktionalitet til at arbejde med json-typen. Derfor kan du, hvis definitionen af indgangsparametrene sker i browseren, direkte der danne json-objektet til SQL-forespørgslen:

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

Til tidligere versioner kan den samme metode bruges til each(hstore), men korrekt «svømning» med escape af komplekse objekter i hstore kan forårsage problemer.

json_populate_recordset

Hvis du på forhånd ved, at data fra det «indgående» json-array skal bruges til at udfylde en bestemt tabel, kan du spare meget på «udredning» af felter og konvertering til de nødvendige typer ved at bruge 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

Denne funktion vil blot «udvikle» det overførte array af objekter i et udtræk uden at basere sig på tabelformatet:

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 hvis mængden af data i den overførte forespørgsel er meget stor, kan det være svært at placere den i et enkelt serialiseret parameter — nogle gange er det endda umuligt, da det kræver en gang tildeling af en stor mængde hukommelseFor example, you may need to collect a large batch of event data from an external system for a long time, and then want to process it in bulk on the database side.

In this case, the best solution will be the use of temporary tables:

CREATE TEMPORARY TABLE tbl(k text, v integer);
...
INSERT INTO tbl(k, v) VALUES($1, $2); -- repeat many many times
...
-- here we do something useful with this entire table

This method is particularly good for occasionally transferring large volumes data.
From the perspective of describing the structure of your data, a temporary table differs from a "regular" table only by one characteristic in the system table pg_class, og der sendes noget til den, vil flytning af denne task til sektionen pg_type, pg_depend, pg_attribute, pg_attrdef, … — nothing at all.

Therefore, in web systems with a large number of short-lived connections, a table like this will generate new system records each time, which are deleted when the connection to the database is closed. As a result, uncontrolled use of TEMP TABLE leads to "bloating" of the tables in pg_catalog and slows down many operations that use them.
Of course, this can be combated with the help of a periodic VACUUM FULL pass through the system catalog tables.

Session variables

Let's assume that processing data from the previous case is complex enough for a single SQL query, but we want to do it quite frequently. That is, we want to use procedural processing in a DO block, but using temporary tables for data transfer would be too cumbersome.

We also cannot use $n-parameters for transmission into an anonymous block. Session variables and the function current_setting.

Up to version 9.2, it was necessary to pre-configure a special namespace custom_variable_classes for "your" session variables. In current versions, you can write something like this:

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

In other supported procedural languages, you can find other solutions.

Do you know any other ways? Share in the comments!

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster