web-dev-qa-db-fra.com

Même fonction dans les clauses SELECT et WHERE

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?

11
Jack Black

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 clause WHERE), 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 le jeu 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 comment cela fonctionne, on peut aller à pg_stat_user_functions et y vérifier calls (étant donné track_functions est défini à 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, il peut donc être en ligne , donc il n'apparaît 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() en IMMUTABLE modifie le comportement (peut-être de façon surprenante), car il sera appelé 13 fois dans les deux requêtes.

22
dezso

Essayez de l'appeler à nouveau:

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