Mesma função na cláusula SELECT e WHERE


11

Pergunta para iniciantes:

Eu tenho uma função cara f(x, y)em duas colunas xey na minha tabela de banco de dados.

Eu quero executar uma consulta que me dê o resultado da função como uma coluna e coloque uma restrição nela, algo como

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

No entanto, isso não funciona, então terei que escrever algo como

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

Isso executará a função cara duas vezes? Qual é a melhor forma de fazer isso?


1
A função é STABLE/ IMMUTABLEou VOLATILE?
Evan Carroll

Respostas:


22

Vamos criar uma função que tenha um efeito colateral para que possamos ver quantas vezes ela é executada:

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 então chame assim:

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)

Como você vê, a função é chamada pelo menos uma vez (da WHEREcláusula) e, quando a condição for verdadeira, mais uma vez para produzir a saída.

Para evitar a segunda execução, você pode fazer o que Edgar sugere - ou seja, agrupar a consulta e filtrar o conjunto de resultados:

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

Para verificar ainda mais como isso funciona, pode-se ir pg_stat_user_functionse verificar callslá (o dado track_functionsé definido como 'all).

Vamos tentar com algo que não tem efeito colateral:

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()é realmente muito simples para que possa ser incorporado , portanto, não aparece na exibição. Vamos torná-lo inlinável:

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

Como parece, a imagem é a mesma com ou sem efeitos colaterais.

Mudar other_one()para IMMUTABLEmudar o comportamento (talvez surpreendentemente) para pior, como será chamado 13 vezes em ambas as consultas.


A decisão de chamar a função novamente pode ser determinada pela presença de uma instrução de efeito colateral no corpo da função? É possível descobrir se uma função com o (s) mesmo (s) parâmetro (s) é (s) chamada (s) uma ou várias vezes por linha, observando o plano de consulta (se, por exemplo, não tiver parte com efeito colateral)?
Andriy M

@AndriyM Eu posso imaginar que sim, mas atualmente não tenho tempo para brincar com um depurador para ver o que realmente é chamado. Irá adicionar um pouco sobre as funções embutidas (que não é o caso que o OP deve esperar, como parece).
Dezso

1
@AndriyM, de acordo com: postgresql.org/docs/9.1/static/sql-createfunction.html, uma função é assumida como VOLATILE se não declarada como IMMUTABLE ou STABLE. VOLATILE indica que o valor da função pode mudar mesmo em uma única verificação de tabela, portanto, nenhuma otimização pode ser feita.
Lennart

5

Tente chamá-lo novamente:

SELECT
     *
FROM (
SELECT
     *,
     f(x, y) AS func
FROM table_name
) a
WHERE a.func < 10;
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.