Mengapa baris yang tidak dimasukkan dalam CTE dapat diperbarui dalam pernyataan yang sama?


12

Di PostgreSQL 9.5, diberikan tabel sederhana yang dibuat dengan:

create table tbl (
    id serial primary key,
    val integer
);

Saya menjalankan SQL untuk menyisipkan nilai, kemudian memperbarui itu dalam pernyataan yang sama:

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;

Hasilnya adalah bahwa UPDATE diabaikan:

testdb=> select * from tbl;
┌────┬─────┐
 id  val 
├────┼─────┤
  1    1 
└────┴─────┘

Kenapa ini? Apakah ini batasan bagian dari standar SQL (yaitu ada di database lain), atau sesuatu yang spesifik untuk PostgreSQL yang mungkin diperbaiki di masa depan? The DENGAN permintaan dokumentasi mengatakan beberapa UPDATE tidak didukung, tetapi tidak menyebutkan INSERT dan UPDATE.

Jawaban:


14

Semua pernyataan dalam CTE terjadi secara hampir bersamaan. Yaitu, mereka didasarkan pada snapshot database yang sama.

Yang UPDATEmelihat keadaan yang sama dari tabel yang mendasarinya sebagai INSERT, yang berarti baris dengan val = 1belum ada. Manual menjelaskan di sini:

Semua pernyataan dieksekusi dengan snapshot yang sama (lihat Bab 13 ), sehingga tidak dapat "melihat" efek satu sama lain pada tabel target.

Setiap pernyataan dapat melihat apa yang dikembalikan oleh CTE lain dalam RETURNINGklausa. Tetapi tabel yang mendasarinya terlihat sama untuk mereka.

Anda memerlukan dua pernyataan (dalam satu transaksi) untuk apa yang Anda coba lakukan. Contoh yang diberikan harus benar-benar hanya satu INSERTuntuk memulai, tetapi itu mungkin karena contoh yang disederhanakan.


14

Ini adalah keputusan implementasi. Ini dijelaskan dalam dokumentasi Postgres, WITHQueries (Common Table Expressions) . Ada dua paragraf yang terkait dengan masalah ini.

Pertama, alasan perilaku yang diamati:

Sub-pernyataan dalam WITHdieksekusi 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 RETURNINGdata adalah satu-satunya cara untuk mengkomunikasikan perubahan antara berbagai WITHsub-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 RETURNINGklausa 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 newvaltabel, 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 INSERTdan kemudian DELETEpada 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).

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.