Même fonction dans les clauses SELECT et WHERE

11

Question débutant:

J'ai une fonction coûteuse f(x, y)sur deux colonnes x et y dans ma table de base de données.

Je veux exécuter une requête qui me donne le résultat de la fonction en tant que colonne et y met une contrainte, quelque chose comme

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

Mais cela ne fonctionne pas, je vais donc devoir écrire quelque chose comme

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

Est-ce que cela exécutera la fonction coûteuse deux fois? Quelle est la meilleure façon de procéder?

Jack Black
la source
1
La fonction est-elle STABLE/ IMMUTABLEou VOLATILE?
Evan Carroll du

Réponses:

22

Créons une fonction qui a un effet secondaire afin que nous puissions voir combien de fois elle est exécutée:

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

Et puis appelez cela comme vous le faites:

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)

Comme vous le voyez, la fonction est appelée au moins une fois (à partir de la WHEREclause), et lorsque la condition est vraie, une fois de plus pour produire la sortie.

Pour éviter la deuxième exécution, vous pouvez faire ce que suggère Edgar - à savoir envelopper la requête et filtrer l'ensemble de résultats:

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

Pour vérifier plus en détail comment cela fonctionne, on peut y aller pg_stat_user_functionset vérifier calls(étant donné track_functionsest réglé sur «tous»).

Essayons avec quelque chose qui n'a aucun effet secondaire:

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()est en fait trop simple pour pouvoir être inséré , il n'apparaît donc pas dans la vue. Rendons-le non-inlinable:

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

En apparence, l'image est la même avec ou sans effets secondaires.

Changer other_one()pour IMMUTABLEchanger le comportement (peut-être surprenant) au pire, car il sera appelé 13 fois dans les deux requêtes.

dezso
la source
La décision d'appeler à nouveau la fonction pourrait-elle être déterminée par la présence d'une instruction d'effet secondaire dans le corps de la fonction? Est-il possible de savoir si une fonction avec le (s) même (s) paramètre (s) est appelée une ou plusieurs fois par ligne en regardant le plan de requête (si, par exemple, elle n'avait pas de partie à effet secondaire)?
Andriy M du
@AndriyM Je peux imaginer oui, mais je n'ai actuellement pas le temps de jouer avec un débogueur pour voir ce qui est réellement appelé. Ajoutera un peu sur les fonctions intégrées (ce qui n'est pas le cas auquel l'OP devrait s'attendre, comme il sonne).
dezso
1
@AndriyM, selon: postgresql.org/docs/9.1/static/sql-createfunction.html une fonction est supposée VOLATILE si elle n'est pas déclarée IMMUTABLE ou STABLE. VOLATILE indique que la valeur de la fonction peut changer même au sein d'une seule analyse de table, donc aucune optimisation ne peut être effectuée.
Lennart
5

Essayez de l'appeler à nouveau:

SELECT
     *
FROM (
SELECT
     *,
     f(x, y) AS func
FROM table_name
) a
WHERE a.func < 10;
Edgar Allan Bayron
la source