PostgreSQL Antipatterns: Übertragung von SĂ€tzen und Auswahl in SQL

Gelegentlich hat der Entwickler die Notwendigkeit einen Parameter-Set oder sogar eine gesamte Auswahl „als Eingabe“ zu ĂŒbergeben. Manchmal begegnen einem sehr seltsame Lösungen fĂŒr dieses Problem.
PostgreSQL Antipatterns: Übertragung von SĂ€tzen und Auswahl in SQL
Lass uns „vom Gegenteil“ ausgehen und schauen, wie man es nicht machen sollte, warum und wie man es besser machen kann.

Direkte „EinschĂŒbe“ von Werten in den Anfragekörper

sehen normalerweise ungefÀhr so aus:

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


 oder so:

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

Über diese Methode wurde viel gesagt, geschrieben und sogar gezeichnet es gibt mehr als genug:

PostgreSQL Antipatterns: Übertragung von SĂ€tzen und Auswahl in SQL

Fast immer fĂŒhrt dies zu einem direkten Weg zu SQL-Injektionen und ĂŒbermĂ€ĂŸiger Belastung der GeschĂ€ftslogik, die gezwungen ist, die Zeichenfolge deiner Anfrage „zusammenzukleben“.

Ein solcher Ansatz kann nur teilweise gerechtfertigt sein, wenn es notwendig ist, Partitionierung in PostgreSQL-Versionen 10 und niedriger zu verwenden, um einen effizienteren Plan zu erhalten. In diesen Versionen wird die Liste der gescannten Abschnitte noch ohne BerĂŒcksichtigung der ĂŒbergebenen Parameter ausschließlich auf Grundlage des Anfragekörpers festgelegt.

$n-Argumente

Verwendung Platzhalter Parameter sind gut, sie erlauben es, PREPARED STATEMENTS, wodurch die Belastung sowohl der GeschĂ€ftslogik (die Anfrage wird nur einmal erstellt und ĂŒbergeben) als auch des DB-Servers (eine wiederholte Analyse und Planung fĂŒr jede Instanz der Anfrage ist nicht erforderlich) reduziert wird.

Eine variable Anzahl von Argumenten

Probleme werden uns erwarten, wenn wir eine im Voraus unbekannte Anzahl von Argumenten ĂŒbergeben möchten:

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

Wenn wir die Anfrage in dieser Form belassen, wird sie uns zwar vor potenziellen Injektionen bewahren, aber trotzdem wird es notwendig sein, die Anfrage zusammenzukleben / zu analysieren fĂŒr jede Variante der Anzahl der Argumente.Das ist schon besser, als es jedes Mal zu tun, aber man könnte darauf verzichten.

Es reicht, nur einen Parameter zu ĂŒbergeben, der eine serialisierte Darstellung eines Arrays enthĂ€lt:

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

Der einzige Unterschied ist die Notwendigkeit, das Argument explizit in den erforderlichen Arraytyp umzuwandeln. Aber das verursacht keine Probleme, da wir ohnehin im Voraus wissen, wohin wir gehen.

Übergabe eines Abfragesatzes (Matrix)

Normalerweise handelt es sich um verschiedene Möglichkeiten, DatensĂ€tze fĂŒr eine „einzeilige Anfrage“ in die Datenbank einzufĂŒgen:

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

Neben den oben beschriebenen Problemen mit der „Zusammenklebe-Anfrage“ kann dies auch zu Out of Memory fĂŒhren. und dem Absturz des Servers. Der Grund ist simpel – beim Argument PG wird zusĂ€tzlicher Speicher reserviert, und die Anzahl der DatensĂ€tze in der Menge wird nur durch die GeschĂ€ftslogik der Anwendung begrenzt. In besonders klinischen FĂ€llen musste ich sehen dass die »nummerischen« Argumente mehr als 9000 Dollar betragen – das mĂŒssen wir nicht so machen.

Lass uns die Abfrage umschreiben und dabei bereits die „zweistufige“ Serialisierung anwenden:

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, im Falle von „komplexen“ Werten innerhalb des Arrays mĂŒssen diese in AnfĂŒhrungszeichen gesetzt werden.
Es ist klar, dass man auf diese Weise die Auswahl mit einer beliebigen Anzahl von Feldern „entfalten“ kann.

unnest, unnest, 


Gelegentlich gibt es Varianten, bei denen anstelle eines „Arrays von Arrays“ mehrere „Spalten-Arrays“ ĂŒbergeben werden, auf die ich hingewiesen habe. im vorherigen Artikel:

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

Bei dieser Methode ist es sehr einfach, unerwartete Ergebnisse zu erzielen, wenn man bei der Erstellung von Wertlisten fĂŒr verschiedene Spalten einen Fehler macht. die zudem von der Serverversion abhĂ€ngen:, abhĂ€ngig von der 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

Seit Version 9.3 gibt es in PostgreSQL vollstĂ€ndige Funktionen zur Arbeit mit dem json-Typ. Daher können Sie, wenn die Definition der Eingabeparameter in Ihrem Browser erfolgt, direkt dort das json-Objekt fĂŒr die SQL-Abfrage formulieren:

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

FĂŒr frĂŒhere Versionen kann dieselbe Methode fĂŒr each(hstore), aber eine korrekte „Einschmelzung“ mit der Escapierung komplexer Objekte in hstore kann Probleme verursachen.

json_populate_recordset

Wenn Sie im Voraus wissen, dass die Daten aus dem „eingehenden“ json-Array zum BefĂŒllen einer bestimmten Tabelle verwendet werden, können Sie erheblich an „Entdereferenzierung“ von Feldern und Typkonvertierung sparen, indem Sie die Funktion json_populate_recordset verwenden:

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

json_to_recordset

Diese Funktion „entfaltet“ einfach das ĂŒbergebene Objektarray in die Auswahl, ohne sich auf das Tabellenformat zu stĂŒtzen:

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

TEMPORÄRE TABELLE

Wenn das Datenvolumen in der ĂŒbergebenen Auswahl jedoch sehr groß ist, ist es schwierig, es in einem einzigen serialisierten Parameter zu verarbeiten – manchmal sogar unmöglich, da dies eine einmalige Zuweisung eines großen Speicherumfangs erfordert.. Zum Beispiel möchten Sie einen großen Datensatz von Ereignissen aus einem externen System ĂŒber einen lĂ€ngeren Zeitraum sammeln und möchten ihn dann einmalig auf der Datenbankseite verarbeiten.

In diesem Fall wÀre die beste Lösung die Nutzung von temporÀren Tabellen:

CREATE TEMPORARY TABLE tbl(k text, v integer);
...
INSERT INTO tbl(k, v) VALUES($1, $2); -- viele Male wiederholen
...
-- hier machen wir etwas NĂŒtzliches mit der gesamten Tabelle

Die Methode ist besonders fĂŒr seltene Übertragungen großer Datenmengen geeignet. von Daten.
Aus Sicht der Beschreibung der Struktur Ihrer Daten unterscheidet sich eine temporĂ€re Tabelle von einer „gewöhnlichen“ nur durch ein einziges Merkmal. in der Systemtabelle pg_class, sondern in pg_type, pg_depend, pg_attribute, pg_attrdef, 
 — so unterscheidet sie sich ĂŒberhaupt nicht.

Deshalb erzeugt eine solche Tabelle in Websystemen mit einer großen Anzahl von kurzlebigen Verbindungen bei jedem einzelnen eine neue systematische Aufzeichnung, die beim Schließen der Verbindung zur Datenbank gelöscht wird. Infolgedessen fĂŒhrt die unkontrollierte Verwendung von TEMP TABLE zu einer „AufblĂ€hung“ der Tabellen im pg_catalog und verlangsamt viele Operationen, die sie verwenden.
NatĂŒrlich kann man dem mit einem regelmĂ€ĂŸigen VACUUM FULL des Systemkatalogs begegnen.

Sitzungsvariablen

Angenommen, die Verarbeitung von Daten aus dem vorherigen Fall ist fĂŒr eine SQL-Abfrage ziemlich komplex, aber wir möchten sie hĂ€ufig durchfĂŒhren. Das heißt, wir möchten die prozedurale Verarbeitung in einem DO-Block, aber die Verwendung der Übertragung von Daten ĂŒber temporĂ€re Tabellen wĂ€re zu umstĂ€ndlich.

Wir können auch die $n-Parameter nicht verwenden, um sie in einen anonymen Block zu ĂŒbergeben. Eine Lösung fĂŒr unser Problem bietet uns die Verwendung von Sitzungsvariablen und der Funktion current_setting.

Bis zur Version 9.2 war es notwendig, ein spezielles Namensraum custom_variable_classes fĂŒr „eigene“ Sitzungsvariablen im Voraus zu konfigurieren. In den aktuellen Versionen kann man etwa so schreiben: 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 anderen unterstĂŒtzten prozeduralen Sprachen können auch andere Lösungen gefunden werden.

Kennt ihr noch andere Methoden? Teilt sie in den Kommentaren!

Gelegentlich hat der Entwickler die Notwendigkeit, eine Menge von Parametern oder sogar eine gesamte Auswahl „zu ĂŒbergeben“.

Quelle: habr.com

ZuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz kaufen, VPS VDS Server đŸ”„ ZuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz kaufen, VPS VDS Server - ProHoster