Fungsi yang sama dalam klausa SELECT dan WHERE

11

Pertanyaan pemula:

Saya memiliki fungsi yang mahal f(x, y)pada dua kolom x dan y dalam tabel database saya.

Saya ingin mengeksekusi kueri yang memberi saya hasil fungsi sebagai kolom dan menempatkan kendala di atasnya, sesuatu seperti

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

Namun ini tidak berhasil, jadi saya harus menulis sesuatu seperti

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

Apakah ini akan menjalankan fungsi mahal dua kali? Apa cara terbaik untuk melakukan ini?

Jack Black
sumber
1
Apakah fungsinya STABLE/ IMMUTABLEatau VOLATILE?
Evan Carroll

Jawaban:

22

Mari kita membuat fungsi yang memiliki efek samping sehingga kita dapat melihat berapa kali dieksekusi:

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

Dan sebut ini seperti yang Anda lakukan:

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)

Seperti yang Anda lihat, fungsi dipanggil setidaknya sekali (dari WHEREklausa), dan ketika kondisinya benar, sekali lagi untuk menghasilkan output.

Untuk menghindari eksekusi kedua, Anda dapat melakukan apa yang disarankan Edgar - yaitu membungkus kueri dan memfilter set hasil:

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

Untuk lebih lanjut memeriksa bagaimana ini bekerja, seseorang dapat pergi ke pg_stat_user_functionsdan memeriksa di callssana (diberikan track_functionsdiatur ke 'semua).

Mari kita coba dengan sesuatu yang tidak memiliki efek samping:

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()sebenarnya terlalu sederhana sehingga dapat diuraikan , oleh karena itu tidak muncul dalam tampilan. Mari kita membuatnya menjadi tidak dapat ditiru:

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

Seperti yang terlihat, gambarnya sama dengan atau tanpa efek samping.

Mengubah other_one()ke IMMUTABLEperubahan perilaku (mungkin mengejutkan) untuk lebih buruk, karena akan disebut 13 kali di kedua query.

dezso
sumber
Bisakah keputusan untuk memanggil fungsi lagi ditentukan oleh kehadiran instruksi efek samping dalam tubuh fungsi? Apakah mungkin untuk mengetahui apakah suatu fungsi dengan parameter yang sama dipanggil sekali atau berkali-kali per baris dengan melihat rencana kueri (jika, misalnya, ia tidak memiliki bagian efek samping)?
Andriy M
@AndriyM Saya bisa membayangkan ya, tetapi saat ini tidak punya waktu untuk bermain dengan debugger untuk melihat apa yang sebenarnya disebut. Akan menambahkan sedikit tentang fungsi-fungsi yang digarisbawahi (yang tidak seharusnya OP, seperti yang terdengar).
dezso
1
@AndriyM, menurut: postgresql.org/docs/9.1/static/sql-createfunction.html suatu fungsi diasumsikan VOLATILE jika tidak dinyatakan sebagai IMMUTABLE atau STABLE. VOLATILE menunjukkan bahwa nilai fungsi dapat berubah bahkan dalam satu pemindaian tabel, jadi tidak ada optimasi yang dapat dilakukan.
Lennart
5

Coba panggil lagi:

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