PostgreSQL Antipatterns: passing sets and selections in SQL

Developers occasionally need to pass a set of parameters or even an entire selection as input. Sometimes, very strange solutions to this task are encountered.
PostgreSQL Antipatterns: passing sets and selections in SQL
Let's approach this from the opposite direction and see how not to do it, why, and how to do it better.

Directly inserting values into the request body

Usually looks something like this:

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

... or like this:

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

There is plenty of information about this method, both written and even illustrated. There's more than enough:

PostgreSQL Antipatterns: passing sets and selections in SQL

Almost always, this is a direct path to SQL injections and excess load on business logic, which has to 'stitch' your query string.

Such an approach can only be partially justified when there's a need for using partitioning in PostgreSQL 10 and earlier versions for obtaining a more efficient plan. In these versions, the list of scanned sections is determined without taking into account the passed parameters, only based on the request body.

$n-arguments

Using placeholders parameters — this is good, as it allows for PREPARED STATEMENTS, reducing the load on both business logic (the query string is formed and sent only once) and the DB server (no need for re-parsing and planning for each instance of the request).

Variable number of arguments

Problems will arise when we want to pass an unknown number of arguments in advance:

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

If we leave the query like this, it may protect us from potential injections, but it will still lead to the necessity of stitching/parsing the query for every variation based on the number of arguments.It's already better than doing it every time, but we can avoid this.

It is enough to pass just one parameter containing a serialized representation of an array.:

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

The only difference is the need to explicitly convert the argument to the required array type. But this poses no problems, since we already know where we are addressing.

Passing a selection (matrix)

Usually, this involves various methods of passing data sets for insertion into the database 'in one request':

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

In addition to the issues described above with 'stitching' the request, this can also lead us to out of memory. and server crashes. The reason is simple — under the PG arguments, additional memory is reserved, while the number of records in the set is limited only by the application's business logic requirements. In particularly extreme cases, I've seen the 'numbered' arguments exceed $9000 — don't do that.

Let's rewrite the query, applying already the 'two-level' serialization:

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;

Yes, in the case of 'complex' values within the array, they need to be enclosed in quotes.
It's clear that this method allows 'unfolding' the selection with an arbitrary number of fields.

unnest, unnest, …

Periodically, there are options to pass several 'arrays of columns' instead of the 'array of arrays', which I mentioned in the previous article:

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

With this method, if you make a mistake when generating value lists for different columns, you can easily get very unexpected results, which also depend on the server version:

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

Starting from version 9.3, PostgreSQL introduced full-fledged functions for working with the json type. Therefore, if your input parameters are defined in the browser, you can form the json object for the SQL query right there:

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

For earlier versions, the same method can be used for each(hstore), but a correct 'unfolding' with escaping complex objects in hstore may cause issues.

json_populate_recordset

If you know in advance that the data from the 'input' json array will be used to populate some table, you can significantly save time on 'dereferencing' fields and casting to the required types by using the json_populate_recordset function:

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

json_to_recordset

And this function simply 'unfolds' the passed array of objects into a selection, without relying on the table format:

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

But if the amount of data in the passed selection is very large, then putting it into one serialized parameter is difficult, and sometimes impossible, as it requires a one-time allocation of a large amount of memoryFor example, you may need to gather a large batch of data from an external system for a long time and then want to process it at once on the database side.

In this case, the best solution would be to use 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 the entire table

This method is particularly good for infrequent transmission of large volumes of data.
In terms of describing the structure of your data, a temporary table differs from a ‘regular’ one only by one characteristic in the system table pg_class, then moving this task to the section pg_type, pg_depend, pg_attribute, pg_attrdef, … — and not at all in any other way.

Therefore, in web systems with a large number of short-lived connections, each of them 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 the ‘bloating’ of tables in pg_catalog and slows down many operations that use them.
Of course, this can be managed through periodic VACUUM FULL passes on the system catalog tables.

Session variables

Suppose handling data from the previous case is complex for a single SQL query, but we want to do it fairly often. 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 passing into an anonymous block. We can turn to session variables and the function current_setting.

Prior to version 9.2, it was necessary to pre-configure a special namespace custom_variable_classes for ‘custom’ 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 other methods? Share in the comments!

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster