Oracle: bagaimana cara UPSERT (memperbarui atau menyisipkan ke dalam tabel?)

293

Operasi UPSERT memperbarui atau menyisipkan baris dalam tabel, tergantung jika tabel sudah memiliki baris yang cocok dengan data:

if table t has a row exists that has key X:
    update t set mystuff... where mykey=X
else
    insert into t mystuff...

Karena Oracle tidak memiliki pernyataan UPSERT tertentu, apa cara terbaik untuk melakukan ini?

Mark Harrison
sumber

Jawaban:

60

Alternatif untuk MERGE ("cara kuno"):

begin
   insert into t (mykey, mystuff) 
      values ('X', 123);
exception
   when dup_val_on_index then
      update t 
      set    mystuff = 123 
      where  mykey = 'X';
end;   
Tony Andrews
sumber
3
@ chotchki: benarkah? Penjelasan akan sangat membantu.
Tony Andrews
15
Masalahnya adalah Anda memiliki jendela di antara sisipan dan pembaruan tempat proses lain berhasil memadamkan penghapusan. Namun saya menggunakan pola ini di atas meja yang tidak pernah dihapus dipecat terhadapnya.
chotchki
2
Baiklah saya setuju. Tidak tahu mengapa itu tidak jelas bagi saya.
Tony Andrews
4
Saya tidak setuju dengan Chotchki. "Durasi Kunci: Semua kunci yang diperoleh oleh pernyataan dalam suatu transaksi ditahan selama durasi transaksi, mencegah gangguan yang merusak termasuk pembacaan yang kotor, pembaruan yang hilang, dan operasi DDL yang merusak dari transaksi bersamaan." Souce: link
yohannc
5
@ yohannc: Saya pikir intinya adalah bahwa kita belum mendapatkan kunci apa pun hanya dengan mencoba dan gagal memasukkan baris.
Tony Andrews
211

The pernyataan MERGE menggabungkan data antara dua tabel. Menggunakan DUAL memungkinkan kita untuk menggunakan perintah ini. Perhatikan bahwa ini tidak dilindungi terhadap akses bersamaan.

create or replace
procedure ups(xa number)
as
begin
    merge into mergetest m using dual on (a = xa)
         when not matched then insert (a,b) values (xa,1)
             when matched then update set b = b+1;
end ups;
/
drop table mergetest;
create table mergetest(a number, b number);
call ups(10);
call ups(10);
call ups(20);
select * from mergetest;

A                      B
---------------------- ----------------------
10                     2
20                     1
Mark Harrison
sumber
57
Rupanya pernyataan "bergabung menjadi" bukanlah atom. Ini dapat menghasilkan "ORA-0001: batasan unik" saat digunakan secara bersamaan. Pemeriksaan untuk keberadaan pertandingan dan penyisipan catatan baru tidak dilindungi oleh kunci, sehingga ada kondisi balapan. Untuk melakukan ini dengan andal, Anda perlu menangkap pengecualian ini dan menjalankan kembali gabungan atau melakukan pembaruan sederhana sebagai gantinya. Di Oracle 10, Anda bisa menggunakan klausa "kesalahan log" untuk membuatnya melanjutkan dengan sisa baris ketika kesalahan terjadi dan mencatat baris yang menyinggung ke tabel lain, bukan hanya berhenti.
Tim Sylvester
1
Hai, saya mencoba menggunakan pola kueri yang sama dalam permintaan saya, tetapi entah bagaimana permintaan saya memasukkan baris duplikat. Saya tidak dapat menemukan informasi lebih lanjut tentang tabel DUAL. Adakah yang bisa memberi tahu saya di mana saya bisa mendapatkan informasi DUAL dan juga tentang penggabungan sintaks?
Shekhar
5
@Shekhar Dual adalah tabel dummy dengan satu baris dan kolomn adp-gmbh.ch/ora/misc/dual.html
YogoZuno
7
@TimSylvester - Oracle menggunakan transaksi, sehingga menjamin snapshot data pada awal transaksi konsisten di seluruh transaksi, menyimpan setiap perubahan yang dibuat di dalamnya. Panggilan serentak ke database menggunakan undo stack; jadi Oracle akan mengatur status final berdasarkan urutan kapan transaksi bersamaan dimulai / selesai. Jadi, Anda tidak akan pernah memiliki kondisi balapan jika pemeriksaan kendala dilakukan sebelum memasukkan terlepas dari berapa banyak panggilan bersamaan dilakukan untuk kode SQL yang sama. Kasus terburuk, Anda mungkin mendapatkan banyak pertengkaran dan Oracle akan membutuhkan waktu lebih lama untuk mencapai kondisi akhir.
Neo
2
@RandyMagruder Apakah ini kasusnya pada tahun 2015 dan kami masih belum dapat melakukan upert dengan andal di Oracle! Apakah Anda tahu solusi aman bersamaan?
dan b
105

Contoh ganda di atas yang ada di PL / SQL sangat bagus karena saya ingin melakukan sesuatu yang serupa, tapi saya ingin sisi klien ... jadi di sini adalah SQL yang saya gunakan untuk mengirim pernyataan serupa langsung dari beberapa C #

MERGE INTO Employee USING dual ON ( "id"=2097153 )
WHEN MATCHED THEN UPDATE SET "last"="smith" , "name"="john"
WHEN NOT MATCHED THEN INSERT ("id","last","name") 
    VALUES ( 2097153,"smith", "john" )

Namun dari perspektif C # ini memberikan lebih lambat daripada melakukan pembaruan dan melihat apakah baris yang terpengaruh adalah 0 dan melakukan memasukkan jika itu.

MyDeveloperDay
sumber
10
Saya kembali ke sini untuk melihat pola ini lagi. Gagal secara diam-diam saat mencoba memasukkan secara bersamaan. Satu penyisipan berlaku, penggabungan kedua tidak menyisipkan atau memperbarui. Namun, pendekatan yang lebih cepat dalam melakukan dua pernyataan terpisah aman.
Synesso
3
pengguna baru oralcle seperti saya mungkin bertanya apa tabel ganda ini melihat ini: stackoverflow.com/q/73751/808698
Hajo Thelen
5
Sayang sekali bahwa dengan pola ini kita perlu menulis data dua kali (John, Smith ...). Dalam hal ini, saya menang apa-apa menggunakan MERGE, dan saya lebih suka menggunakan lebih sederhana DELETEkemudian INSERT.
Nicolas Barbulesco
@NicolasBarbulesco jawaban ini tidak perlu menulis data dua kali: stackoverflow.com/a/4015315/8307814
whyer
@NicolasBarbulescoMERGE INTO mytable d USING (SELECT 1 id, 'x' name from dual) s ON (d.id = s.id) WHEN MATCHED THEN UPDATE SET d.name = s.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);
whyer
46

Alternatif lain tanpa mengecek pengecualian:

UPDATE tablename
    SET val1 = in_val1,
        val2 = in_val2
    WHERE val3 = in_val3;

IF ( sql%rowcount = 0 )
    THEN
    INSERT INTO tablename
        VALUES (in_val1, in_val2, in_val3);
END IF;
Brian Schmitt
sumber
Solusi yang Anda berikan tidak bekerja untuk saya. Apakah% rowcount hanya berfungsi dengan kursor eksplisit?
Synesso
Bagaimana jika pembaruan mengembalikan 0 baris yang dimodifikasi karena catatan sudah ada dan nilainya sama?
Adriano Varoli Piazza
10
@Adriano: sql% rowcount masih akan kembali> 0 jika klausa WHERE cocok dengan baris mana pun, bahkan jika pembaruan tidak benar-benar mengubah data apa pun pada baris itu.
Tony Andrews
Tidak berfungsi: PLS-00207: pengidentifikasi 'COUNT', diterapkan pada SQL kursor implisit, bukan atribut kursor legal
Patrik Beck
Kesalahan Sintaks di sini :(
ilmirons
27
  1. masukkan jika tidak ada
  2. memperbarui:
    
Masukkan ke dalam tabel saya (id1, t1) 
  SELECT 11, 'x1' DARI DUAL 
  WHERE NOT EXISTS (PILIH id1 DARI mytble WHERE id1 = 11); 

UPDATE mytable SET t1 = 'x1' WHERE id1 = 11;
test1
sumber
26

Tidak ada jawaban yang diberikan sejauh ini aman dalam menghadapi akses bersamaan , seperti yang ditunjukkan dalam komentar Tim Sylvester, dan akan menimbulkan pengecualian jika terjadi balapan. Untuk memperbaikinya, kombo sisipkan / perbarui harus dibungkus dengan semacam pernyataan loop, sehingga dalam kasus pengecualian semuanya dicoba lagi.

Sebagai contoh, inilah cara kode Grommit dapat dibungkus dalam satu lingkaran agar aman saat dijalankan secara bersamaan:

PROCEDURE MyProc (
 ...
) IS
BEGIN
 LOOP
  BEGIN
    MERGE INTO Employee USING dual ON ( "id"=2097153 )
      WHEN MATCHED THEN UPDATE SET "last"="smith" , "name"="john"
      WHEN NOT MATCHED THEN INSERT ("id","last","name") 
        VALUES ( 2097153,"smith", "john" );
    EXIT; -- success? -> exit loop
  EXCEPTION
    WHEN NO_DATA_FOUND THEN -- the entry was concurrently deleted
      NULL; -- exception? -> no op, i.e. continue looping
    WHEN DUP_VAL_ON_INDEX THEN -- an entry was concurrently inserted
      NULL; -- exception? -> no op, i.e. continue looping
  END;
 END LOOP;
END; 

NB Dalam mode transaksi SERIALIZABLE, yang saya tidak rekomendasikan btw, Anda mungkin mengalami ORA-08177: sebaliknya, tidak dapat membuat serial akses untuk pengecualian transaksi ini .

Eugene Beresovsky
sumber
3
Luar biasa! Akhirnya, secara bersamaan mengakses jawaban yang aman. Adakah cara untuk menggunakan konstruksi semacam itu dari klien (mis. Dari klien Java)?
Sebien
1
Maksudmu tidak harus memanggil proc yang tersimpan? Nah, dalam hal ini Anda juga bisa menangkap pengecualian Java tertentu dan coba lagi dalam loop Java. Ini jauh lebih nyaman di Jawa daripada Oracle SQL.
Eugene Beresovsky
Maaf: saya tidak cukup spesifik. Tapi Anda mengerti cara yang benar. Saya mengundurkan diri untuk melakukan seperti yang baru saja Anda katakan. Tapi saya tidak 100% puas karena menghasilkan lebih banyak permintaan SQL, lebih banyak klien / server pulang pergi. Ini bukan solusi yang baik untuk kinerja. Tetapi tujuan saya adalah membiarkan pengembang Java proyek saya menggunakan metode saya untuk mengunggulkan dalam tabel apa pun (saya tidak bisa membuat satu prosedur tersimpan PLSQL per tabel, atau satu prosedur per jenis upsert).
Sebien
@Sebien Saya setuju, akan lebih baik untuk memilikinya dienkapsulasi di ranah SQL, dan saya pikir Anda bisa melakukannya. Saya hanya tidak secara sukarela mencari tahu untuk Anda ... :) Plus, pada kenyataannya pengecualian ini mungkin akan terjadi kurang dari sekali dalam bulan biru, jadi Anda seharusnya tidak melihat dampak pada kinerja dalam 99,9% kasus. Kecuali ketika melakukan pengujian beban tentu saja ...
Eugene Beresovsky
24

Saya ingin jawaban Grommit, kecuali itu membutuhkan nilai dupe. Saya menemukan solusi yang mungkin muncul sekali: http://forums.devshed.com/showpost.php?p=1182653&postcount=2

MERGE INTO KBS.NUFUS_MUHTARLIK B
USING (
    SELECT '028-01' CILT, '25' SAYFA, '6' KUTUK, '46603404838' MERNIS_NO
    FROM DUAL
) E
ON (B.MERNIS_NO = E.MERNIS_NO)
WHEN MATCHED THEN
    UPDATE SET B.CILT = E.CILT, B.SAYFA = E.SAYFA, B.KUTUK = E.KUTUK
WHEN NOT MATCHED THEN
    INSERT (  CILT,   SAYFA,   KUTUK,   MERNIS_NO)
    VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); 
Hubbitus
sumber
2
Apakah maksud Anda INSERT (B.CILT, B.SAYFA, B.KUTUK, B.MERNIS_NO) VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO); ?
Matteo
Tentu. Terima kasih. Tetap.
Hubbitus
Untungnya Anda telah mengedit jawaban Anda! :) suntingan saya sayangnya menolak stackoverflow.com/review/suggested-edits/7555674
Matteo
9

Catatan tentang dua solusi yang menyarankan:

1) Masukkan, jika pengecualian maka perbarui,

atau

2) Perbarui, jika sql% rowcount = 0 lalu masukkan

Pertanyaan apakah memasukkan atau memperbarui terlebih dahulu juga tergantung pada aplikasi. Apakah Anda mengharapkan lebih banyak sisipan atau lebih banyak pembaruan? Yang paling mungkin berhasil harus pergi dulu.

Jika Anda memilih yang salah, Anda akan mendapatkan banyak indeks yang tidak perlu dibaca. Bukan masalah besar tapi masih sesuatu yang perlu dipertimbangkan.

AnthonyVO
sumber
sql% notfound adalah preferensi pribadi saya
Arturo Hernandez
8

Saya telah menggunakan contoh kode pertama selama bertahun-tahun. Perhatikan notfound daripada count.

UPDATE tablename SET val1 = in_val1, val2 = in_val2
    WHERE val3 = in_val3;
IF ( sql%notfound ) THEN
    INSERT INTO tablename
        VALUES (in_val1, in_val2, in_val3);
END IF;

Kode di bawah ini adalah kode yang mungkin baru dan lebih baik

MERGE INTO tablename USING dual ON ( val3 = in_val3 )
WHEN MATCHED THEN UPDATE SET val1 = in_val1, val2 = in_val2
WHEN NOT MATCHED THEN INSERT 
    VALUES (in_val1, in_val2, in_val3)

Pada contoh pertama pembaruan melakukan pencarian indeks. Itu harus, untuk memperbarui baris yang tepat. Oracle membuka kursor implisit, dan kami menggunakannya untuk membungkus sisipan yang sesuai sehingga kami tahu bahwa sisipan hanya akan terjadi ketika kunci tidak ada. Tetapi sisipan adalah perintah independen dan harus melakukan pencarian kedua. Saya tidak tahu cara kerja dari perintah penggabungan tetapi karena perintah adalah satu unit, Oracle bisa mengeksekusi memasukkan yang benar atau memperbarui dengan pencarian indeks tunggal.

Saya pikir penggabungan lebih baik ketika Anda memiliki beberapa pemrosesan yang harus dilakukan yang berarti mengambil data dari beberapa tabel dan memperbarui tabel, mungkin menyisipkan atau menghapus baris. Tetapi untuk kasus baris tunggal, Anda dapat mempertimbangkan kasus pertama karena sintaksis lebih umum.

Arturo Hernandez
sumber
0

Salin & tempel contoh untuk memasang satu tabel ke tabel lainnya, dengan MERGE:

CREATE GLOBAL TEMPORARY TABLE t1
    (id VARCHAR2(5) ,
     value VARCHAR2(5),
     value2 VARCHAR2(5)
     )
  ON COMMIT DELETE ROWS;

CREATE GLOBAL TEMPORARY TABLE t2
    (id VARCHAR2(5) ,
     value VARCHAR2(5),
     value2 VARCHAR2(5))
  ON COMMIT DELETE ROWS;
ALTER TABLE t2 ADD CONSTRAINT PK_LKP_MIGRATION_INFO PRIMARY KEY (id);

insert into t1 values ('a','1','1');
insert into t1 values ('b','4','5');
insert into t2 values ('b','2','2');
insert into t2 values ('c','3','3');


merge into t2
using t1
on (t1.id = t2.id) 
when matched then 
  update set t2.value = t1.value,
  t2.value2 = t1.value2
when not matched then
  insert (t2.id, t2.value, t2.value2)  
  values(t1.id, t1.value, t1.value2);

select * from t2

Hasil:

  1. b 4 5
  2. c 3 3
  3. a 1 1
Bechyňák Petr
sumber
-3

Coba ini,

insert into b_building_property (
  select
    'AREA_IN_COMMON_USE_DOUBLE','Area in Common Use','DOUBLE', null, 9000, 9
  from dual
)
minus
(
  select * from b_building_property where id = 9
)
;
r4bitt
sumber
-6

Dari http://www.praetoriate.com/oracle_tips_upserts.htm :

"Di Oracle9i, UPSERT dapat menyelesaikan tugas ini dalam satu pernyataan:"

INSERT
FIRST WHEN
   credit_limit >=100000
THEN INTO
   rich_customers
VALUES(cust_id,cust_credit_limit)
   INTO customers
ELSE
   INTO customers SELECT * FROM new_customers;
Segera
sumber
14
-1 Khas Don Burleson cr @ p Saya takut - ini adalah memasukkan ke dalam satu tabel atau yang lain, tidak ada "upsert" di sini!
Tony Andrews