Ini adalah keputusan implementasi. Ini dijelaskan dalam dokumentasi Postgres, WITH
Queries (Common Table Expressions) . Ada dua paragraf yang terkait dengan masalah ini.
Pertama, alasan perilaku yang diamati:
Sub-pernyataan dalam WITH
dieksekusi bersamaan dengan satu sama lain dan dengan permintaan utama . Oleh karena itu, ketika menggunakan pernyataan pengubah data WITH
, urutan pembaruan yang ditentukan benar-benar terjadi tidak dapat diprediksi. Semua pernyataan dieksekusi dengan snapshot yang sama (lihat Bab 13), sehingga mereka tidak dapat "melihat" efek satu sama lain pada tabel target. Ini mengurangi efek dari ketidakpastian urutan pembaruan baris yang sebenarnya, dan berarti bahwa RETURNING
data adalah satu-satunya cara untuk mengkomunikasikan perubahan antara berbagai WITH
sub-pernyataan dan permintaan utama. Contoh dari ini adalah bahwa di ...
Setelah saya mengirim saran ke pgsql-docs , Marko Tiikkaja menjelaskan (yang setuju dengan jawaban Erwin):
Sisipkan-perbarui dan sisipkan-hapus kasing tidak berfungsi karena UPDATE dan HAPUS tidak memiliki cara untuk melihat baris yang disisipkan karena snapshot mereka telah diambil sebelum INSERT terjadi. Tidak ada yang tak terduga tentang kedua kasus ini.
Jadi alasan mengapa pernyataan Anda tidak diperbarui dapat dijelaskan oleh paragraf pertama di atas (tentang "foto"). Apa yang terjadi ketika Anda memodifikasi CTE adalah bahwa semuanya dan kueri utama dieksekusi dan "melihat" snapshot yang sama dari data (tabel), karena mereka langsung sebelum eksekusi pernyataan. CTE dapat meneruskan informasi tentang apa yang mereka masukkan / perbarui / hapus satu sama lain dan ke permintaan utama dengan menggunakan RETURNING
klausa tetapi mereka tidak dapat melihat perubahan dalam tabel secara langsung. Jadi mari kita lihat apa yang terjadi dalam pernyataan Anda:
WITH newval AS (
INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;
Kami memiliki 2 bagian, CTE ( newval
):
-- newval
INSERT INTO tbl(val) VALUES (1) RETURNING id
dan permintaan utama:
-- main
UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id
Alur eksekusi adalah seperti ini:
initial data: tbl
id │ val
(empty)
/ \
/ \
/ \
newval: \
tbl (after newval) \
id │ val \
1 │ 1 |
|
newval: returns |
id |
1 |
\ |
\ |
\ |
main query
Akibatnya, ketika kueri utama bergabung dengan tbl
(seperti terlihat dalam snapshot) dengan newval
tabel, itu bergabung dengan tabel kosong dengan tabel 1-baris. Jelas itu memperbarui 0 baris. Jadi pernyataan itu tidak pernah benar-benar datang untuk memodifikasi baris yang baru dimasukkan dan itulah yang Anda lihat.
Solusi dalam kasus Anda, adalah menulis ulang pernyataan untuk memasukkan nilai yang benar di tempat pertama atau menggunakan 2 pernyataan. Satu yang memasukkan dan satu detik untuk memperbarui.
Ada situasi lain yang serupa, seperti jika pernyataan memiliki INSERT
dan kemudian DELETE
pada baris yang sama. Penghapusan akan gagal karena alasan yang persis sama.
Beberapa kasus lain, dengan pembaruan-pembaruan dan pembaruan-hapus dan perilakunya dijelaskan dalam paragraf berikut, di halaman dokumen yang sama.
Mencoba memperbarui baris yang sama dua kali dalam satu pernyataan tidak didukung. Hanya satu modifikasi yang terjadi, tetapi tidak mudah (dan kadang-kadang tidak mungkin) untuk memprediksi yang mana. Ini juga berlaku untuk menghapus baris yang sudah diperbarui dalam pernyataan yang sama: hanya pembaruan yang dilakukan. Karena itu, Anda umumnya harus menghindari mencoba memodifikasi satu baris dua kali dalam satu pernyataan. Khususnya hindari menulis DENGAN sub-pernyataan yang dapat mempengaruhi baris yang sama diubah oleh pernyataan utama atau sub-pernyataan saudara. Efek dari pernyataan seperti itu tidak akan dapat diprediksi.
Dan dalam balasan dari Marko Tiikkaja:
Pembaruan-perbarui dan perbarui-hapus kasus secara eksplisit tidak disebabkan oleh detail implementasi yang mendasari yang sama (seperti kasus insert-update dan insert-delete).
Kasus pembaruan-pembaruan tidak berfungsi karena secara internal terlihat seperti masalah Halloween, dan Postgres tidak memiliki cara untuk mengetahui tupel mana yang boleh diperbarui dua kali dan yang mana yang dapat memperkenalkan kembali masalah Halloween.
Jadi alasannya sama (bagaimana memodifikasi CTE diterapkan dan bagaimana masing-masing CTE melihat snapshot yang sama) tetapi detailnya berbeda dalam 2 kasus ini, karena mereka lebih kompleks dan hasilnya dapat diprediksi dalam kasus pembaruan-pembaruan.
Dalam insert-update (seperti kasus Anda) dan insert-delete yang serupa hasilnya dapat diprediksi. Hanya penyisipan yang terjadi karena operasi kedua (perbarui atau hapus) tidak memiliki cara untuk melihat dan memengaruhi baris yang baru disisipkan.
Solusi yang disarankan adalah sama untuk semua kasus yang mencoba mengubah baris yang sama lebih dari sekali: Jangan lakukan itu. Entah menulis pernyataan yang memodifikasi setiap baris sekali atau menggunakan pernyataan terpisah (2 atau lebih).