PostgreSQL Antipatterns: Übergeben von Mengen und Auswahlen an SQL

Von Zeit zu Zeit muss der Entwickler eine Reihe von Parametern an die Anforderung oder sogar eine ganze Auswahl von "Eingaben" übergeben. Es gibt manchmal sehr seltsame Lösungen für dieses Problem.

Gehen wir "vom Gegenteil" aus und sehen, wie es sich nicht lohnt, warum und wie Sie es besser machen können.

Direkte Einfügung von Werten in den Anfragekörper


Normalerweise sieht es so aus:

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

... oder so:

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

Über diese Methode wird reichlich gesagt, geschrieben und sogar gezeichnet :



Fast immer ist dies ein direkter Pfad zur SQL-Injection und eine zusätzliche Belastung der Geschäftslogik, die gezwungen ist, Ihre Abfragezeichenfolge zu „kleben“.

Ein solcher Ansatz kann nur teilweise gerechtfertigt sein, wenn die Verwendung von Abschnitten in Versionen von PostgreSQL 10 und niedriger erforderlich ist, um einen effizienteren Plan zu erhalten. In diesen Versionen wird die Liste der gescannten Abschnitte auch ohne Berücksichtigung der übermittelten Parameter nur auf Basis des Anforderungskörpers ermittelt.

$ n Argumente


Die Verwendung von Parameterplatzhaltern ist gut. Sie können PREPARED STATEMENTS verwenden , um die Last sowohl für die Geschäftslogik (eine Abfragezeichenfolge wird nur einmal generiert und übertragen) als auch für den Datenbankserver zu verringern (erneutes Parsen und Planen für jede Instanz der Anforderung ist nicht erforderlich).

Variable Anzahl von Argumenten


Probleme werden auf uns warten, wenn wir im Voraus eine unbekannte Anzahl von Argumenten übergeben möchten:

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

Wenn Sie die Anfrage in diesem Formular belassen, werden wir zwar vor potenziellen Injektionen geschützt, es ist jedoch erforderlich, die Anfrage für jede Option anhand der Anzahl der Argumente zu analysieren. Schon besser als jedes Mal, aber darauf kann man verzichten.

Es reicht aus, nur einen Parameter zu übergeben, der die serialisierte Darstellung des Arrays enthält :

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

Der einzige Unterschied besteht darin, dass das Argument explizit in den gewünschten Array-Typ konvertiert werden muss. Dies bereitet aber keine Probleme, da wir bereits im Vorfeld wissen, wo wir ansprechen.

Probentransfer (Matrizen)


Normalerweise sind dies alle möglichen Optionen zum Übertragen von Datensätzen zum Einfügen in die Datenbank „in einer Anforderung“:

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

Zusätzlich zu den oben beschriebenen Problemen beim erneuten Festhalten der Anforderung kann dies auch zu nicht genügend Arbeitsspeicher und einem Serverabsturz führen. Der Grund ist einfach: PG reserviert zusätzlichen Speicher für die Argumente, und die Anzahl der Datensätze in der Gruppe ist nur durch die angewendete Geschäftslogik von Wishlist begrenzt. In besonders klinischen Fällen musste man "nummerierte" Argumente sehen, die größer als 9.000 US-Dollar waren - das war nicht nötig.

Wir schreiben die Anfrage um und wenden dabei die zweistufige Serialisierung an :

 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, bei "komplexen" Werten innerhalb des Arrays müssen diese in Anführungszeichen gesetzt werden.
Es ist klar, dass Sie auf diese Weise die Auswahl mit einer beliebigen Anzahl von Feldern "erweitern" können.

unnest, unnest, ...


In regelmäßigen Abständen gibt es Übertragungsoptionen anstelle eines "Arrays von Arrays" mehrerer "Spaltenarrays", die ich in einem früheren Artikel erwähnt habe :

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

Mit dieser Methode, die beim Generieren von Wertelisten für verschiedene Spalten einen Fehler macht, ist es sehr einfach, völlig unerwartete Ergebnisse zu erzielen , die auch von der Serverversion abhängen:

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


Ab Version 9.3 hat PostgreSQL umfassende Funktionen für die Arbeit mit dem Typ json eingeführt. Wenn die Definition der Eingabeparameter in Ihrem Browser erfolgt, können Sie daher direkt dort ein json-Objekt für die SQL-Abfrage erstellen:

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

Für frühere Versionen kann für jeden (hstore) dieselbe Methode verwendet werden, aber die korrekte "Faltung" mit dem Entkommen komplexer Objekte in hstore kann Probleme verursachen.

json_populate_recordset


Wenn Sie im Voraus wissen, dass die Daten aus dem "Eingabe" -Json-Array eine Art Tabelle füllen, können Sie mit der Funktion "json_populate_recordset" viel beim "Dereferenzieren" der Felder und beim Umwandeln in die erforderlichen Typen sparen:

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

json_to_recordset


Und diese Funktion erweitert einfach das übertragene Array von Objekten in die Auswahl, ohne sich auf das Tabellenformat zu verlassen:
 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 die Datenmenge in der übertragenen Stichprobe jedoch sehr groß ist, ist es schwierig und manchmal unmöglich, sie in einen serialisierten Parameter umzuwandeln, da eine einmalige Zuweisung einer großen Speichermenge erforderlich ist. Beispielsweise müssen Sie lange, lange Zeit ein großes Datenpaket zu Ereignissen von einem externen System erfassen und anschließend einmal auf der Datenbankseite verarbeiten.

In diesem Fall wäre die beste Lösung, temporäre Tabellen zu verwenden :

 CREATE TEMPORARY TABLE tbl(k text, v integer); ... INSERT INTO tbl(k, v) VALUES($1, $2); --  -  ... --   -       

Die Methode eignet sich für die seltene Übertragung großer Datenmengen .
Unter dem Gesichtspunkt der Beschreibung der Struktur seiner Daten unterscheidet sich die temporäre Tabelle von der "normalen" Tabelle nur um ein Merkmal in der Systemtabelle pg_class und in pg_type, pg_depend, pg_attribute, pg_attrdef, ... - überhaupt nichts.

In Websystemen mit einer großen Anzahl kurzlebiger Verbindungen generiert eine solche Tabelle daher jedes Mal neue Systemdatensätze, die bei geschlossener Verbindung zur Datenbank gelöscht werden. Infolgedessen führt die unkontrollierte Verwendung von TEMP TABLE zum "Aufblähen" von Tabellen in pg_catalog und verlangsamt viele Operationen, die sie verwenden.
Dies kann natürlich mit Hilfe des periodischen Durchlaufs VACUUM FULL durch die Systemkatalogtabellen bekämpft werden.

Sitzungsvariablen


Angenommen, die Verarbeitung von Daten aus dem vorherigen Fall ist für eine einzelne SQL-Abfrage recht kompliziert, aber Sie möchten dies häufig genug tun. Das heißt, wir möchten die prozedurale Verarbeitung im DO-Block verwenden , aber die Datenübertragung durch temporäre Tabellen ist zu teuer.

Wir werden auch keine $ n-Parameter für die Übertragung in den anonymen Block verwenden können. Die Sitzungsvariablen und die Funktion current_setting helfen uns, aus dieser Situation herauszukommen.

Vor Version 9.2 musste ein custom_variable_classes- Namespace für "Ihre" Sitzungsvariablen vorkonfiguriert werden. In den aktuellen Versionen können Sie so etwas 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 

Andere unterstützte Verfahrenssprachen können andere Lösungen finden.

Kennst du mehr Möglichkeiten? Teilen Sie in den Kommentaren!

Source: https://habr.com/ru/post/de481122/


All Articles