Stessa funzione nella clausola SELECT e WHERE


11

Domanda per principianti:

Ho una funzione costosa f(x, y)su due colonne xey nella mia tabella del database.

Voglio eseguire una query che mi dà il risultato della funzione come una colonna e pone un vincolo su di essa, qualcosa del genere

SELECT *, f(x, y) AS func FROM table_name WHERE func < 10;

Tuttavia, questo non funziona, quindi dovrò scrivere qualcosa del genere

SELECT *, f(x, y) AS func FROM table_name WHERE f(x, y) < 10;

Questo eseguirà la costosa funzione due volte? Qual'è il miglior modo per farlo?


1
La funzione è STABLE/ IMMUTABLEo VOLATILE?
Evan Carroll,

Risposte:


22

Creiamo una funzione che ha un effetto collaterale in modo da poter vedere quante volte viene eseguita:

CREATE OR REPLACE FUNCTION test.this_here(val integer)
    RETURNS numeric
    LANGUAGE plpgsql
AS $function$
BEGIN
    RAISE WARNING 'I am called with %', val;
    RETURN sqrt(val);
END;
$function$;

E poi chiamalo come fai tu:

SELECT this_here(i) FROM generate_series(1,10) AS t(i) WHERE this_here(i) < 2;

WARNING:  I am called with 1
WARNING:  I am called with 1
WARNING:  I am called with 2
WARNING:  I am called with 2
WARNING:  I am called with 3
WARNING:  I am called with 3
WARNING:  I am called with 4
WARNING:  I am called with 5
WARNING:  I am called with 6
WARNING:  I am called with 7
WARNING:  I am called with 8
WARNING:  I am called with 9
WARNING:  I am called with 10
    this_here     
──────────────────
                1
  1.4142135623731
 1.73205080756888
(3 rows)

Come vedi, la funzione viene chiamata almeno una volta (dalla WHEREclausola) e quando la condizione è vera, ancora una volta per produrre l'output.

Per evitare la seconda esecuzione, puoi fare ciò che suggerisce Edgar , ovvero avvolgere la query e filtrare il set di risultati:

SELECT * 
  FROM (SELECT this_here(i) AS val FROM generate_series(1,10) AS t(i)) x 
 WHERE x.val < 2;

WARNING:  I am called with 1
... every value only once ...
WARNING:  I am called with 10

Per verificare ulteriormente come funziona, si può andare a pg_stat_user_functionse controllare callslì (dato track_functionsè impostato su 'tutto).

Proviamo con qualcosa che non ha effetti collaterali:

CREATE OR REPLACE FUNCTION test.simple(val numeric)
 RETURNS numeric
 LANGUAGE sql
AS $function$
SELECT sqrt(val);
$function$;

SELECT simple(i) AS v 
  FROM generate_series(1,10) AS t(i)
 WHERE simple(i) < 2;
-- output omitted

SELECT * FROM pg_stat_user_functions WHERE funcname = 'simple';
-- 0 rows

simple()è in realtà troppo semplice, quindi può essere integrato , quindi non appare nella vista. Rendiamolo inlinabile:

CREATE OR REPLACE FUNCTION test.other_one(val numeric)
 RETURNS numeric
 LANGUAGE sql
AS $function$
SELECT 1; -- to prevent inlining
SELECT sqrt(val);
$function$;

SELECT other_one(i) AS v
  FROM generate_series(1,10) AS t(i)
 WHERE other_one(i) < 2;

SELECT * FROM pg_stat_user_functions ;
 funcid  schemaname  funcname   calls  total_time  self_time 
────────┼────────────┼───────────┼───────┼────────────┼───────────
 124311  test        other_one     13       0.218      0.218

SELECT *
  FROM (SELECT other_one(i) AS v FROM generate_series(1,10) AS t(i)) x 
 WHERE v < 2;

SELECT * FROM pg_stat_user_functions ;
 funcid  schemaname  funcname   calls  total_time  self_time 
────────┼────────────┼───────────┼───────┼────────────┼───────────
 124311  test        other_one     23       0.293      0.293

A quanto pare, l'immagine è la stessa con o senza effetti collaterali.

Passare other_one()a IMMUTABLEcambia il comportamento (forse sorprendentemente) in peggio, poiché verrà chiamato 13 volte in entrambe le query.


La decisione di richiamare nuovamente la funzione potrebbe essere determinata dalla presenza di un'istruzione con effetti collaterali nel corpo della funzione? È possibile scoprire se una funzione con gli stessi parametri è chiamata una o più volte per riga guardando il piano di query (se, ad esempio, non ha avuto una parte che ha effetti collaterali)?
Andriy M,

@AndriyM Posso immaginare di sì, ma al momento non ho tempo per giocare con un debugger per vedere come viene effettivamente chiamato. Aggiungerà un po 'di funzioni incorporate (che non è il caso che l'OP dovrebbe aspettarsi, come sembra).
dezso

1
@AndriyM, secondo: postgresql.org/docs/9.1/static/sql-createfunction.html una funzione è considerata VOLATILE se non dichiarata IMMUTABILE o STABILE. VOLATILE indica che il valore della funzione può cambiare anche all'interno di una singola scansione della tabella, quindi non è possibile effettuare ottimizzazioni.
Lennart,

5

Prova a chiamarlo di nuovo:

SELECT
     *
FROM (
SELECT
     *,
     f(x, y) AS func
FROM table_name
) a
WHERE a.func < 10;
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.