Fungsi PostgreSQL tidak dieksekusi ketika dipanggil dari dalam CTE

14

Hanya berharap untuk mengkonfirmasi pengamatan saya dan mendapatkan penjelasan tentang mengapa ini terjadi.

Saya memiliki fungsi yang didefinisikan sebagai:

CREATE OR REPLACE FUNCTION "public"."__post_users_id_coin" ("coins" integer, "userid" integer) RETURNS TABLE (id integer) AS '
UPDATE
users
SET
coin = coin + coins
WHERE
userid = users.id
RETURNING
users.id' LANGUAGE "sql" COST 100 ROWS 1000
VOLATILE
RETURNS NULL ON NULL INPUT
SECURITY INVOKER

Ketika saya memanggil fungsi ini dari CTE, itu mengeksekusi perintah SQL tetapi tidak memicu fungsi, misalnya:

WITH test AS
(SELECT * FROM __post_users_id_coin(10, 1))

SELECT
1 -- Select 1 but update not performed

Di sisi lain, jika saya memanggil fungsi dari CTE dan kemudian memilih hasil CTE (atau memanggil fungsi langsung tanpa CTE) itu mengeksekusi perintah SQL dan memicu fungsi, misalnya:

WITH test AS
(SELECT * FROM __post_users_id_coin(10, 1))

SELECT
*
FROM
test -- Select result and update performed

atau

SELECT * FROM __post_users_id_coin(10,1)

Karena saya tidak terlalu peduli dengan hasil fungsi (hanya perlu melakukan pembaruan) apakah ada cara agar ini berfungsi tanpa memilih hasil CTE?

Andy
sumber

Jawaban:

11

Itu semacam perilaku yang diharapkan. CTE terwujud tetapi ada pengecualian.

Jika CTE tidak dirujuk dalam kueri induk maka tidak terwujud sama sekali. Anda dapat mencoba ini misalnya dan itu akan berjalan dengan baik:

WITH not_executed AS (SELECT 1/0),
     executed AS (SELECT 1)
SELECT * FROM executed ;

Kode disalin dari komentar di posting blog Craig Ringer:
CTE PostgreSQL adalah pagar pengoptimalan .


Sebelum mencoba ini dan beberapa pertanyaan serupa, saya pikir pengecualiannya adalah: "ketika CTE tidak dirujuk dalam kueri induk atau dalam CTE lain dan tidak merujuk dirinya sendiri CTE lain". Jadi, jika Anda ingin CTE dieksekusi tetapi hasilnya tidak ditampilkan dalam hasil query, saya pikir ini akan menjadi solusi (merujuknya pada CTE lain).

Tapi sayangnya, itu tidak bekerja seperti yang saya harapkan:

WITH test AS
    (SELECT * FROM __post_users_id_coin(10, 1)),
  execute_test AS 
    (TABLE test)
SELECT 1 ;     -- no, it doesn't do the update

dan karena itu, "aturan pengecualian" saya tidak benar. Ketika CTE direferensikan oleh CTE lain dan tidak satupun dari mereka yang direferensikan oleh query induk, situasinya lebih rumit dan saya tidak yakin persis apa yang terjadi dan kapan CTE terwujud. Saya juga tidak dapat menemukan referensi untuk kasus-kasus seperti itu dalam dokumentasi.


Saya tidak melihat solusi yang lebih baik daripada menggunakan apa yang sudah Anda sarankan:

SELECT * FROM __post_users_id_coin(10, 1) ;

atau:

WITH test AS
    (SELECT * FROM __post_users_id_coin(10, 1))
SELECT *
FROM test ;

Jika fungsi memperbarui beberapa baris dan Anda mendapatkan banyak baris (dengan 1) di hasilnya, Anda bisa mengumpulkan untuk mendapatkan satu baris:

SELECT MAX(1) AS result FROM __post_users_id_coin(10, 1) ;

tapi saya lebih suka memiliki hasil fungsi yang melakukan pembaruan dikembalikan, dengan SELECT *sebagai contoh Anda, jadi apa pun panggilan kueri ini tahu jika ada pembaruan dan apa perubahan dalam tabel itu.

ypercubeᵀᴹ
sumber
Mari kita lanjutkan diskusi ini dalam obrolan .
ypercubeᵀᴹ
4

Ini diharapkan, perilaku yang terdokumentasi.

Tom Lane menjelaskannya di sini.

Didokumentasikan dalam manual di sini:

Pernyataan pengubah data di WITHdieksekusi tepat sekali, dan selalu sampai selesai , terlepas dari apakah kueri utama membaca semua (atau memang ada) dari output mereka. Perhatikan bahwa ini berbeda dari aturan untuk SELECTdi WITH: seperti yang dinyatakan dalam bagian sebelumnya, eksekusi a SELECThanya dilakukan sejauh permintaan utama menuntut outputnya .

Penekanan berani saya. "Memodifikasi data" adalah INSERT, UPDATEdan DELETEkueri. (Berbeda dengan SELECT.). Manual sekali lagi:

Anda dapat menggunakan laporan data memodifikasi ( INSERT, UPDATE, atau DELETE) di WITH.

Fungsi yang tepat

CREATE OR REPLACE FUNCTION public.__post_users_id_coin (_coins integer, _userid integer)
  RETURNS TABLE (id integer) AS
$func$
UPDATE users u
SET    coin = u.coin + _coins  -- see below
WHERE  u.id = _userid
RETURNING u.id
$func$ LANGUAGE sql COST 100 ROWS 1000 STRICT;

Saya menjatuhkan klausa default (noise) dan STRICTmerupakan sinonim singkat untukRETURNS NULL ON NULL INPUT .

Pastikan entah bagaimana nama parameter tidak bertentangan dengan nama kolom. Saya menambahkan _, tapi itu hanya preferensi pribadi saya.

Jika coinbisa NULLsaya sarankan:

SET    coin = CASE WHEN coin IS NULL THEN _coins ELSE coin + _coins END

Jika users.idadalah kunci utama, maka keduanya RETURNS TABLEtidak ROWs 1000masuk akal. Hanya satu baris yang dapat diperbarui / dikembalikan. Tapi itu semua di samping poin utama.

Panggilan yang tepat

Tidak masuk akal untuk menggunakan RETURNINGklausa dan mengembalikan nilai dari fungsi Anda jika Anda akan mengabaikan nilai yang dikembalikan dalam panggilan. Juga tidak masuk akal untuk menguraikan baris yang dikembalikan dengan SELECT * FROM ...jika Anda mengabaikannya.

Cukup kembalikan konstanta skalar ( RETURNING 1), tentukan fungsinya sebagai RETURNS int(atau jatuhkan RETURNINGsama sekali dan buatlah RETURNS void) dan sebut denganSELECT my_function(...)

Larutan

Karena kamu ...

tidak terlalu peduli dengan hasilnya

.. hanya SELECTbentuk konstan CTE. Ini dijamin akan dieksekusi selama itu direferensikan di luar SELECT(langsung atau tidak langsung).

WITH test AS (SELECT __post_users_id_coin(10, 1))
SELECT 1 FROM test;

Jika Anda benar-benar memiliki fungsi set-return dan masih tidak peduli dengan hasilnya:

WITH test AS (SELECT * FROM __post_users_id_coin(10, 1))
SELECT 1 FROM test LIMIT 1;

Tidak perlu mengembalikan lebih dari 1 baris. Fungsi ini masih dipanggil.

Akhirnya, tidak jelas mengapa Anda membutuhkan CTE untuk memulai. Mungkin hanya bukti konsep.

Erat terkait:

Jawaban terkait pada SO:

Dan pertimbangkan:

Erwin Brandstetter
sumber
Hebat, penggemar berat dan merasa terhormat untuk memiliki jawaban Anda juga Erwin. Saya menggunakan CTE karena saya melakukan INSERTsebelum ke UPDATEdalam fungsi pembungkus yang sama - tidak ada transaksi yang tersedia.
Andy
Bagus. Hanya aq: adalah testdi WITH test AS (SELECT * FROM __post_users_id_coin(10, 1)) SELECT ... LIMIT 1;dianggap sebagai CTE memodifikasi atau tidak?
ypercubeᵀᴹ
@ ypercubeᵀᴹ: A SELECTbukan "pengubah data" menurut terminologi CTE. Saya menambahkan beberapa klarifikasi di atas. Adalah tanggung jawab pengguna jika ia menambahkan kode ke fungsi yang mengubah data di balik tirai.
Erwin Brandstetter