Kita baru saja mengenal INSERT ... ON DUPLICATE KEY UPDATE yang memperbarui baris lama secara halus. MySQL menyediakan satu pesaing yang tampak melakukan hal serupa tetapi dengan pendekatan yang jauh lebih kasar: REPLACE. Perintah ini menghapus baris lama yang bertabrakan kunci, lalu menyisipkan baris baru dari nol — bukan memperbarui baris lama di tempat. Perbedaan mekanisme tersebut punya konsekuensi nyata pada data, dan pada Chapter ini kita akan melihat buktinya secara langsung: kolom yang tidak kita sebut ikut kehilangan nilainya, id AUTO_INCREMENT berperilaku khusus, dan foreign key bisa menghalangi (atau lebih buruk: diam-diam mendukung) penghapusan tersebut. Kita tutup dengan perbandingan tegas melawan ON DUPLICATE KEY UPDATE sebagai dasar memilih di antara keduanya.

Menyiapkan Tabel Latihan #

Kita pakai tabel dengan tiga kolom yang nilai bawaannya mudah diamati: stok yang punya DEFAULT 0, dan tanggal_masuk yang boleh NULL:

sql
CREATE DATABASE belajar_dml;
USE belajar_dml;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

Database changed
sql
CREATE TABLE produk (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(100) NOT NULL,
  harga DECIMAL(10,2) NOT NULL,
  stok INT NOT NULL DEFAULT 0,
  tanggal_masuk DATE
);

INSERT INTO produk (id, nama, harga, stok, tanggal_masuk) VALUES
  (1, 'Buku Tulis', 4500.00, 120, '2026-01-10'),
  (2, 'Pensil 2B', 3000.00, 150, '2026-01-11');

SELECT * FROM produk ORDER BY id;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

+----+------------+---------+------+---------------+
| id | nama       | harga   | stok | tanggal_masuk |
+----+------------+---------+------+---------------+
|  1 | Buku Tulis | 4500.00 |  120 | 2026-01-10    |
|  2 | Pensil 2B  | 3000.00 |  150 | 2026-01-11    |
+----+------------+---------+------+---------------+
2 rows in set (0.00 sec)

Kedua baris sengaja diisi lengkap — stok dan tanggal_masuk terisi penuh. Nilai-nilai itulah yang akan menjadi korban demonstrasi utama kita.

REPLACE Menghapus Dulu, Baru Menyisipkan #

Sintaks REPLACE persis seperti INSERT — hanya keyword-nya yang berbeda. Kita kirim data untuk Pensil 2B (id 2) yang sudah ada, tetapi kali ini kita hanya menyertakan tiga kolom:

sql
REPLACE INTO produk (id, nama, harga) VALUES (2, 'Pensil 2B', 3200.00);

SELECT * FROM produk ORDER BY id;

Output yang Dihasilkan:

text
Query OK, 2 rows affected (0.00 sec)

+----+------------+---------+------+---------------+
| id | nama       | harga   | stok | tanggal_masuk |
+----+------------+---------+------+---------------+
|  1 | Buku Tulis | 4500.00 |  120 | 2026-01-10    |
|  2 | Pensil 2B  | 3200.00 |    0 | NULL          |
+----+------------+---------+------+---------------+
2 rows in set (0.00 sec)

Mari kita bedah setiap bagiannya:

  1. Baris 1 (REPLACE INTO produk (id, nama, harga)): Struktur identik dengan INSERT INTO ... (kolom). Server mencoba menyisipkan baris baru persis seperti INSERT biasa.
  2. Baris 1 lanjutan (VALUES (2, 'Pensil 2B', 3200.00)): Karena id 2 sudah dimiliki baris lama, terjadilah pelanggaran primary key. Di titik inilah REPLACE berbeda: baris lama DIHAPUS, lalu baris baru disisipkan.
  3. Konsekuensi (stok dan tanggal_masuk tidak disebut): Baris baru disisipkan dari nol, sehingga kolom yang tidak kita berikan nilainya memakai nilai bawaan tabel — bukan nilai lama. stok kembali ke 0 dan tanggal_masuk kembali ke NULL.

Hasil SELECT memperlihatkan kedua korban itu: stok 150 dan tanggal 2026-01-11 lenyap, padahal statement kita sama sekali tidak menyentuh kedua kolom tersebut. Respons 2 rows affected juga khas: satu untuk penghapusan baris lama dan satu untuk penyisipan baris baru — bukan dua baris data yang berubah.

REPLACE Tanpa Kunci Tabrakan #

Bila data yang kita kirim tidak menabrak kunci mana pun, REPLACE tinggal menjadi INSERT biasa. Contoh berikut mengirim produk baru tanpa menyebut id:

sql
REPLACE INTO produk (nama, harga, stok) VALUES ('Stipo', 6000.00, 40);

SELECT * FROM produk ORDER BY id;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

+----+------------+---------+------+---------------+
| id | nama       | harga   | stok | tanggal_masuk |
+----+------------+---------+------+---------------+
|  1 | Buku Tulis | 4500.00 |  120 | 2026-01-10    |
|  2 | Pensil 2B  | 3200.00 |    0 | NULL          |
|  3 | Stipo      | 6000.00 |   40 | NULL          |
+----+------------+---------+------+---------------+
3 rows in set (0.00 sec)

Tidak ada baris lama yang dihapus — responsnya 1 row affected untuk satu baris baru — dan AUTO_INCREMENT mengisi id 3 seperti biasa. Perilaku ini menegaskan mekanisme dasarnya: bagian “replace” hanya bekerja pada tabrakan kunci. Menariknya, bila tabrakan terjadi pada unique key BUKAN primary key dan kolom id tidak kita sebut, baris pengganti tetap mendapat id baru — baris lama dihapus penuh, identitas penomorannya tidak dipertahankan.

Berdampingan: REPLACE versus ON DUPLICATE KEY UPDATE #

Pertarungan yang jujur membutuhkan dua baris dengan kondisi awal yang sama. Kita siapkan dua produk lengkap datanya, lalu timpa keduanya dengan nilai baru — yang satu lewat REPLACE, yang satu lewat upsert:

sql
INSERT INTO produk (id, nama, harga, stok, tanggal_masuk) VALUES
  (4, 'Map Plastik', 8500.00, 35, '2026-01-12'),
  (5, 'Kertas HVS A4', 25000.00, 15, '2026-01-13');

REPLACE INTO produk (id, nama, harga) VALUES (4, 'Map Plastik', 9000.00);

INSERT INTO produk (id, nama, harga) VALUES (5, 'Kertas HVS A4', 26000.00) AS baru ON DUPLICATE KEY UPDATE harga = baru.harga;

SELECT id, nama, harga, stok, tanggal_masuk FROM produk ORDER BY id;

Output yang Dihasilkan:

text
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

Query OK, 2 rows affected (0.00 sec)

Query OK, 2 rows affected (0.00 sec)

+----+---------------+----------+------+---------------+
| id | nama          | harga    | stok | tanggal_masuk |
+----+---------------+----------+------+---------------+
|  1 | Buku Tulis    |  4500.00 |  120 | 2026-01-10    |
|  2 | Pensil 2B     |  3200.00 |    0 | NULL          |
|  3 | Stipo         |  6000.00 |   40 | NULL          |
|  4 | Map Plastik   |  9000.00 |    0 | NULL          |
|  5 | Kertas HVS A4 | 26000.00 |   15 | 2026-01-13    |
+----+---------------+----------+------+---------------+
5 rows in set (0.00 sec)

Kedua statement sama-sama mengubah harga produknya, tetapi nasib kolom lainnya berbeda total. Map Plastik yang ditimpa REPLACE kehilangan stok dan tanggal masuknya — 0 dan NULL — karena barisnya dihapus lalu dibuat ulang. Kertas HVS A4 yang ditimpa upsert mempertahankan stok 15 dan tanggal_masuk 2026-01-13, karena upsert hanya mengubah kolom harga yang disebut di klausa UPDATE. Ringkasan perbedaan keduanya:

AspekREPLACEON DUPLICATE KEY UPDATE
Mekanisme saat kunci bertabrakanhapus baris lama, sisip baris baruperbarui baris lama di tempat
Kolom yang tidak disebutkembali ke nilai default / NULLnilainya bertahan
Rows affected (satu baris lama)2 (1 hapus + 1 sisip)2 bila nilai berubah, 0 bila sama
Kecocokan penggunaanmenimpa baris sepenuhnyapembaruan sebagian kolom

Kesimpulan praktisnya: REPLACE tepat bila kita memang ingin mengganti seluruh isi baris dengan data baru — misalnya memuat ulang baris dari file master yang selalu dianggap kebenaran mutlak. Untuk pembaruan sebagian kolom sambil mempertahankan kolom lain, upsert adalah satu-satunya pilihan yang aman.

Kapan Jangan Memakai REPLACE: Foreign Key #

Karena REPLACE mengandung penghapusan di dalamnya, seluruh aturan DELETE terhadap foreign key berlaku juga di sini. Kita buat tabel anak yang merujuk produk.id:

sql
CREATE TABLE item_nota (
  id INT AUTO_INCREMENT PRIMARY KEY,
  id_produk INT NOT NULL,
  jumlah INT NOT NULL,
  FOREIGN KEY (id_produk) REFERENCES produk (id)
);

INSERT INTO item_nota (id_produk, jumlah) VALUES (2, 3);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Tabel item_nota kini memegang satu baris nota untuk Pensil 2B (id 2). Kita coba timpa produk tersebut dengan REPLACE:

sql
-- @error
REPLACE INTO produk (id, nama, harga) VALUES (2, 'Pensil 2B', 3300.00);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`belajar_dml`.`item_nota`, CONSTRAINT `item_nota_ibfk_1` FOREIGN KEY (`id_produk`) REFERENCES `produk` (`id`))
Bye

Galat ERROR 1451 yang sama dengan chapter DELETE muncul: langkah penghapusan di dalam REPLACE ditolak foreign key karena masih ada baris anak yang merujuk baris tersebut. Perintah yang tampak seperti INSERT ternyata membawa seluruh beban DELETE — dan inilah jebakannya: kita tidak akan menemukannya pada tabel yang berdiri sendiri, hanya pada skema ber-relasi.

Membersihkan Database Latihan #

Latihan selesai. Dua tabel — produk dan item_nota — ikut terjatuh bersama database-nya:

sql
DROP DATABASE belajar_dml;

Output yang Dihasilkan:

text
Query OK, 2 rows affected (0.00 sec)

Referensi #

Empat perintah DML — INSERT, SELECT, UPDATE, DELETE — beserta varian upsert-nya kini lengkap kita kuasai. Satu persoalan terakhir di section ini menyentuh seluruh tabel sekaligus: bagaimana cara mengosongkan tabel dengan benar? Chapter penutup membahas TRUNCATE TABLE — perintah yang menghapus seluruh baris, me-reset AUTO_INCREMENT kembali ke 1, dan menolak bekerja pada tabel yang di-referensikan foreign key.