Using functions that return result set in SELECT part of an SQL query in PostgreSQL

[Pagina in italiano]
Lately I had to write a query that transform each record of a table into a serie of records. Specifically, from a table with two colums (first is a key, second is a list of car plates) I had to extract a result set with two colums: the key and a single plate. For all record that had more than one plate, I was asked to duplicate the record in order to have a single car plate as second column.

In other words:

postgres=# create temporary table t (key varchar primary key, plates varchar);
postgres=# insert into t values ('000000','AA888BB CC777DD GG333JJ'), ('111111','ZZ888KK');
INSERT 0 2
postgres=# select * from t;
  key   |         plates
--------+-------------------------
 000000 | AA888BB CC777DD GG333JJ
 111111 | ZZ888KK

what I wanted was:

  key   |         plate
--------+-------------------------
 000000 | AA888BB
 000000 | CC777DD
 000000 | GG333JJ
 111111 | ZZ888KK

The solution I found is:

postgres=# select key,
    unnest(regexp_split_to_array(plates, E'\\s+')) AS plate from t;
  key   |  plate
--------+---------
 000000 | AA888BB
 000000 | CC777DD
 000000 | GG333JJ
 111111 | ZZ888KK

What does this query? First, it convert a list (plates) into an array using a space separator, and second, it convert the arrary into a relation of possibly many records.

But, I didn’t like this solution, and moreover, I did not even undertand it. So, a few questions arose:

  1. how is it possible that postgresql allow me to specify a function tha return a set in the SELECT part instead of the FROM part? When I studied SQL, I learned that FROM is for specifying all my data sources (relations), and SELECT for specifying what to display and eventually how to format them.
  2. how does postgresql choose creating a cartesian product multiplying a first element (a single value “key”) and a second one (a relation “plate”)?
  3. how postgresql define this second relation that is not a fixed one since it depends on a filed taken from the current record? I.e., for each “key” there is a specific relation “plate”. Furthermore, if this is really a cartesia product, addin a new unnest would create 4×3 records. Let’s try:
    postgres=# select key,
        unnest(regexp_split_to_array(plates, E'\\s+')) AS plate1,
        unnest(regexp_split_to_array(plates, E'\\s+')) AS plate2 from t;
      key   | plate1  | plate2
    --------+---------+---------
     000000 | AA888BB | AA888BB
     000000 | CC777DD | CC777DD
     000000 | GG333JJ | GG333JJ
     111111 | ZZ888KK | ZZ888KK
  4. why this has not lead to a new cartesian product? May this be related to IMMUTABLE functions like, probably, the unnest one? (I think to remember that IMMUTABLE functions are functions that do not chage result when you call it using the same arguments. In this case postgresql would avoid to call them many time and directly use the result. But why it does not do a new product?) Let’s test is differently, with another array:
    postgres=# select key,
        unnest(regexp_split_to_array(plates, E'\\s+')) AS plate1,
        unnest('{1,2}'::int[]) AS array2 from t;
      key   | plate1  | array2
    --------+---------+--------
     000000 | AA888BB |      1
     000000 | CC777DD |      2
     000000 | GG333JJ |      1
     000000 | AA888BB |      2
     000000 | CC777DD |      1
     000000 | GG333JJ |      2
     111111 | ZZ888KK |      1
     111111 | ZZ888KK |      2

    here, a cartesian product is made.

So, without much understanding of that is going on, I contacted a couple of mailing lists until I got an answer by Tom Lane (here).

The answer contains a few points:

  1. the best way to wite this query is to use the LATERAL subqueries, moving the function that create the arry from the SELECT to the FROM part, and to replace the two functions unnest+regexp_split_to_array with a single function regexp_split_to_table:
    select key, targa
        from t,
        lateral regexp_split_to_table(plates, E'\\s+') as plate;
  2. utilizing functions that return a result set in SELECT part, is a hangover from Berkeley QUEL. This part of the code is not something Lane is happy. Moreover he notes that with postgresql 10, the code that manage these functions that returns more rows has been rewritten in order to better isolate it from other code (see the link in his email)
  3. the cartesian product is not made for every function called. The final number of records is base on the least common multiple of all periods of the functions results. This explain why using two function that produce 3 record, at the end produce a 3 records results, while using a function that produce 3 records and a function that produce 2 record, will end in a 6 records results.

Usare delle funzioni che restituiscono result set nella parte SELECT di costrutti SQL in PostgreSQL

[English page]
Di recente ho dovuto scrivere una query che trasformasse ogni record di una tabella, in una serie di record. In particolare, da una tabella che ha due colonne (la prima è la chiave, la seconda è un elenco di targhe) ho dovuto estrarre un result set che avesse due colonne: la chiave e una sola targa. Per quei record che hanno più targhe, si doveva arrivare a duplicare il record in modo da avere la coppia chiave/targa per ciascuna delle targhe nell’elenco relativo.

Altrimenti detto:

postgres=# create temporary table t (chiave varchar primary key, targhe varchar);
postgres=# insert into t values ('000000','AA888BB CC777DD GG333JJ'), ('111111','ZZ888KK');
INSERT 0 2
postgres=# select * from t;
 chiave |         targhe
--------+-------------------------
 000000 | AA888BB CC777DD GG333JJ
 111111 | ZZ888KK

quello che volevo era:

 chiave |         targa
--------+-------------------------
 000000 | AA888BB
 000000 | CC777DD
 000000 | GG333JJ
 111111 | ZZ888KK

la soluzione che avevo trovato era la seguente:

postgres=# select chiave,
    unnest(regexp_split_to_array(targhe, E'\\s+')) AS targa from t;
 chiave |  targa
--------+---------
 000000 | AA888BB
 000000 | CC777DD
 000000 | GG333JJ
 111111 | ZZ888KK

Cosa fa la query che ho scritto? Primo, converte in array la colonna targhe usando il separatore spazio, secondo converte l’array in una relazione di vari record.

Ma questa soluzione non mi piaceva, e neppure la capivo granché. Mi sono quindi fatto alcune domande:

  1. come mai posso mettere nella parte SELECT (e non nella FROM) una relazione? Difatti quando studi l’SQL impari che le relazioni (tabelle, viste, eccetera) vanno nella parte FROM che si usa proprio per dire da dove prendere i dati, mentre nella parte SELECT inserisci cosa visualizzare.
  2. come decide, postgresql, di fare un prodotto cartesiano tra il primo elemento (singolo campo “chiave”) e il secondo (relazione “targa”)?
  3. come fa postgresql a definire questa seconda relazione che non è costante, ma dipende dal primo campo del record corrente? Vale a dire che per ogni “chiave” c’è una relazione “targa” diversa. E poi, se veramente viene fatto il prodotto cartesiano, dovrei poter aggiungere un secondo unnest e produrre 4×3 record. Proviamo:
    postgres=# select chiave,
    unnest(regexp_split_to_array(targhe, E'\\s+')) AS targa1,
    unnest(regexp_split_to_array(targhe, E'\\s+')) AS targa2 from t;
     chiave | targa1  | targa2
    --------+---------+---------
     000000 | AA888BB | AA888BB
     000000 | CC777DD | CC777DD
     000000 | GG333JJ | GG333JJ
     111111 | ZZ888KK | ZZ888KK
  4. come mai non ha fatto un ulteriore prodotto? Può avere a che fare con il concetto di funzioni IMMUTABLE alla quale forse unnest appartiene? (Mi pare di ricordare che si chiamino IMMUTABLE le funzioni che a fronte dello stesso input, danno lo stesso output. In questo caso postgresql potrebbe non richiamare nuovamente la unnest perché tanto conosce già il risultato. Ma perché non fa un ulteriore prodotto?)controprova, faccio l’unnest con un altro array:
    postgres=# select chiave,
        unnest(regexp_split_to_array(targhe, E'\\s+')) AS targa1,
        unnest('{1,2}'::int[]) AS array2 from t;
     chiave | targa1  | array2
    --------+---------+--------
     000000 | AA888BB |      1
     000000 | CC777DD |      2
     000000 | GG333JJ |      1
     000000 | AA888BB |      2
     000000 | CC777DD |      1
     000000 | GG333JJ |      2
     111111 | ZZ888KK |      1
     111111 | ZZ888KK |      2

    qui il prodotto l’ha fatto.

Con tanta confusione in testa, ho contattato alcune mailing list fino ad ottenere la risposta chiarificatrice di Tom Lane (qui).

La risposta prevede vari punti:

  1. la query migliore da fare prevede l’utilizzo del LATERAL e lo spostamento della funzione che restituisce l’array dalla parte SELECT alla parte FROM, nonché la sostituzione delle due chiamate unnest+regexp_split_to_array con una sola regexp_split_to_table:
    select chiave, targa
        from t,
            lateral regexp_split_to_table(targhe, E'\\s+') as targa;
  2. il fatto che si possano mettere delle funzioni che restituiscono più di un valore nella parte SELECT è un dovuto alla derivazione di postgresql dal Berkeley QUEL. Di questa parte di codice Tom non è particolarmente contento. Tra l’altro fa notare che in postgresql 10, il codice per la gestione di queste funzioni che restituiscono più valori, è stato notevolmente cambiato per centralizzarlo e isolarlo dal resto (vedi link nel suo email)
  3. il prodotto cartesiano non viene fatto per ciascuna delle funzioni di questo tipo chiamate. Il numero finale di record viene in realtà prodotto in base alla ricerca di un minimo comune multiplo della cardinalità dei result set delle varie funzioni. Questo spiega perché se ho due funzioni (nel mio caso, uguali) che restituiscono entrambe tre record, il mcm è 3, se ne ho una da 3 una da 2, il mcm è 6.

logrotate e postrotate

Il logrotate è un strumento molto utile che gestisce i file di log delle varie applicazioni. Ogni giorno viene eseguito dal cron e, in base alla configurazione, archivia tutti i log. Tra le cose interessanti c’è la possibilità di eseguire un comando dopo l’archiviazione del log (e prima della sua eventuale compressione). L’operazione che viene eseguita normalmente a fine archiviazione, è quella di avvisare l’applicazione di utilizzare un nuovo file di log.
Ad esempio, nel file /etc/logrotate.d/rsyslog di Debian c’è scritto:

postrotate
invoke-rc.d rsyslog rotate > /dev/null
endscript

oppure, nel file /etc/logrotate.d/apache2 c’è scritto:

postrotate
if /etc/init.d/apache2 status > /dev/null ; then \
/etc/init.d/apache2 reload > /dev/null; \
fi;
endscript

Il manuale, a proposito del postrotate, dice:
The lines between postrotate and endscript (both of which must appear on lines by themselves) are executed (using /bin/sh) after the log file is rotated. These directives may only appear inside a log file definition. Normally, the absolute path to the log file is passed as first argument to the  script. If  sharedscripts is specified, whole pattern is passed to the script. See also prerotate. See sharedscripts and nosharedscripts for error handling.

Il secondo esempio dovrebbe fare scattare un campanello d’allarme riguardo la sintassi da usare nel postrotate: non si tratta di uno script composto da vari comandi, ma di linee che vengono eseguite tramite la shell. Allora, se il comando è uno solo, come nel caso di rsyslog, non c’è problema, ma se si usano varie linee, è necessario concatenarle tramite il backslash a fine riga, e utilizzare sempre il separatore «;» tra i comandi.