Insert, atau Update Jika Sudah Ada
Kebutuhan yang umum: masukkan sebuah baris, tapi jika baris dengan kunci yang sama sudah ada, perbarui baris itu saja. Tanpa UPSERT, kamu harus menulis SELECT dulu, lalu bercabang ke INSERT atau UPDATE: dua kali bolak-balik ke database dan ada race condition di antaranya.
UPSERT di SQLite melakukannya dalam satu pernyataan:
Saat pertama kali dijalankan, barisnya dimasukkan. Jalankan lagi dengan harga berbeda dan sku yang sama, dan baris yang ada diperbarui di tempat. Tidak ada duplikat, tidak ada error.
Anatomi ON CONFLICT
Bentuk lengkapnya:
INSERT INTO table (...) VALUES (...)
ON CONFLICT(conflict_target) DO UPDATE SET col = expr, ...
WHERE condition;
Ada tiga bagian penting:
conflict_target: kolom atau kolom-kolom dengan constraintUNIQUEatauPRIMARY KEYyang kamu perkirakan akan bertabrakan. SQLite memakai ini untuk memilih index mana yang diawasi.DO UPDATE SET ...: apa yang diubah pada baris yang ada saat tabrakan terjadi. (AtauDO NOTHINGuntuk melewatinya diam-diam.)WHEREopsional: kondisi tambahan yang harus benar agar update benar-benar dijalankan.
Target konflik harus cocok dengan constraint unik yang sungguhan. ON CONFLICT(price) tidak akan bisa dikompilasi jika price tidak unik, karena SQLite tidak punya apa pun untuk mendeteksi konflik.
DO NOTHING: Insert Jika Belum Ada, Selain Itu Lewati
Varian yang lebih sederhana. Berguna saat kamu mengisi data awal atau mencatat peristiwa dan duplikat cukup diabaikan diam-diam:
Insert kedua menabrak event_id yang sama dan biasanya akan memunculkan UNIQUE constraint failed. Dengan DO NOTHING, SQLite cukup melewatinya. Tidak ada exception, tidak ada baris yang terpengaruh.
Inilah "insert idempoten" yang sering dikerjakan orang dengan INSERT OR IGNORE. DO NOTHING milik UPSERT melakukan tugas yang sama dan lebih mudah dipadukan dengan klausa WHERE dan RETURNING.
Tabel Semu excluded
Saat konflik terjadi, tiba-tiba ada dua baris yang terlibat: baris yang sudah ada di tabel, dan baris baru yang coba kamu masukkan. SQLite memberimu cara untuk membicarakan keduanya.
- Nama kolom tanpa awalan (
price,name) merujuk ke baris yang sudah ada. excluded.columnmerujuk ke baris masuk yang ditolak.
quantity = quantity + excluded.quantity dibaca sebagai "kuantitas yang ada ditambah kuantitas yang baru." Setelah dua insert, A-100 punya kuantitas 8. Pola ini, yaitu mengakumulasi nilai ke baris yang sudah ada, adalah salah satu trik UPSERT yang paling berguna.
UPSERT Bersyarat dengan WHERE
WHERE di bagian akhir memungkinkanmu melewati update kecuali suatu kondisi terpenuhi. Kondisi ini dievaluasi terhadap baris yang sudah ada (dan bisa merujuk ke excluded.* untuk baris yang masuk):
Baris baru membawa updated_at yang lebih lama, jadi WHERE bernilai false dan update dilewati. Baris yang ada tetap memakai harganya yang lebih baru. Tukar tanggalnya, dan update akan berjalan. Inilah pola standar "hanya timpa dengan data yang lebih baru".
Upsert Banyak Baris
VALUES bisa berisi banyak baris, dan ON CONFLICT berlaku untuk masing-masing secara terpisah:
A-100 bertabrakan dan diperbarui. A-200 dan A-300 baru, jadi dimasukkan. Satu pernyataan, hasil campuran insert dan update. Ini cara yang rapi untuk menyinkronkan sekumpulan data dari sumber eksternal.
UPSERT vs INSERT OR REPLACE
INSERT OR REPLACE terlihat seperti melakukan hal yang sama. Padahal tidak.
notes hilang. INSERT OR REPLACE menghapus baris 1 seluruhnya dan memasukkan baris baru: setiap kolom yang tidak kamu cantumkan kembali ke NULL atau nilai default-nya. Perintah ini juga memicu trigger DELETE dan merambat lewat foreign key ON DELETE.
UPSERT mempertahankan barisnya:
notes masih ada. Hanya kolom yang disebut di SET yang berubah. Jadikan UPSERT pilihan default; pakai INSERT OR REPLACE hanya saat kamu benar-benar menginginkan semantik hapus lalu masukkan ulang.
Beberapa Target Konflik
Jika sebuah baris bisa bertabrakan pada lebih dari satu constraint, kamu bisa merangkai beberapa klausa ON CONFLICT:
Constraint mana pun yang terpicu lebih dulu yang menang, dan DO UPDATE milik cabang itu yang dijalankan. Dalam praktiknya, sebagian besar tabel punya satu target konflik yang jelas, yaitu primary key atau satu kolom unik, dan kamu jarang butuh lebih dari satu klausa.
Jebakan Umum
Beberapa hal yang sering menjebak:
- Tanpa unique index yang cocok, tidak ada UPSERT.
ON CONFLICT(col)mengharuskancolberupaPRIMARY KEYatau punya constraintUNIQUE. Jika tidak, SQLite memunculkan error "no such constraint." DO UPDATEtidak berjalan jika tidak ada konflik. Ini adalah alternatif dari insert, bukan perilaku tambahan. Saat sebuah kunci pertama kali muncul, hanya insert yang berjalan.excludedhanya bisa dibaca. Kamu bisa membaca darinya, tapi tidak bisa menulis ke sana. TargetSETselalu baris yang sudah ada.- Rowid
INTEGER PRIMARY KEYyang dibuat otomatis. Jika kamu tidak memberikan id, setiap insert mendapat id baru, jadi tidak ada yang bisa berkonflik. UPSERT hanya masuk akal saat kolom yang berkonflik punya nilai pasti yang diberikan oleh pemanggil.
Berikutnya: RETURNING
UPSERT tidak memberi tahu baris mana yang dimasukkan dan mana yang diperbarui, atau seperti apa nilai akhirnya. Untuk itu, kamu butuh klausa RETURNING: klausa ini mengembalikan baris yang terpengaruh dalam pernyataan yang sama, tanpa perlu SELECT susulan. Itu yang berikutnya.
Pertanyaan yang sering diajukan
Apa itu UPSERT di SQLite?
UPSERT adalah INSERT yang berubah menjadi UPDATE (atau tidak melakukan apa-apa) saat insert itu akan melanggar constraint UNIQUE atau PRIMARY KEY. Kamu menulisnya sebagai INSERT ... ON CONFLICT(column) DO UPDATE SET ... atau DO NOTHING. SQLite mendukungnya sejak versi 3.24.0 (2018).
Apa itu tabel excluded pada UPSERT SQLite?
excluded adalah tabel semu khusus yang berisi baris yang coba kamu masukkan. Di dalam DO UPDATE SET ..., kamu merujuk baris yang sudah ada dengan nama kolomnya dan baris yang ditolak sebagai excluded.column. Jadi SET price = excluded.price berarti 'timpa price dengan nilai apa pun yang dibawa INSERT baru itu'.
Apa perbedaan INSERT OR REPLACE dan UPSERT?
INSERT OR REPLACE menghapus baris yang berkonflik lalu memasukkan baris baru. Itu memicu trigger DELETE, merusak foreign key yang memakai ON DELETE CASCADE, dan mengembalikan setiap kolom ke nilai default-nya. UPSERT memperbarui baris yang ada di tempat, jadi hanya kolom yang kamu sebut di SET yang berubah. Utamakan UPSERT kecuali kamu memang ingin menghapus lalu memasukkan ulang.
Bisakah melakukan upsert banyak baris sekaligus di SQLite?
Bisa. INSERT INTO t(...) VALUES (...), (...), (...) ON CONFLICT(col) DO UPDATE SET ... berjalan dengan baik. Setiap baris diperiksa terhadap target konflik secara terpisah, dan baris excluded di dalam DO UPDATE merujuk ke baris masuk mana pun yang memicu konflik.