TL;DR
Perintah DELETE di SQL dipakai buat menghapus baris dari tabel, dengan sintaks DELETE FROM nama_tabel WHERE kondisi. Kalau klausa WHERE dilupakan, DELETE bakal menghapus SEMUA baris di tabel itu dan nggak bisa di-undo. Kebiasaan paling aman: tulis dulu SELECT * FROM tabel WHERE kondisi buat lihat baris mana yang bakal kena, baru ganti SELECT * jadi DELETE setelah hasilnya cocok, dan jalankan di dalam transaction (BEGIN ... ROLLBACK/COMMIT).
DELETE di SQL dipakai buat menghapus baris dari tabel. Sintaksnya DELETE FROM nama_tabel WHERE kondisi;
Lupa nulis WHERE? Seluruh isi tabel kehapus.
Nggak ada Ctrl+Z. Nggak ada tempat sampah. Kalau kamu udah COMMIT dan nggak punya backup, datanya beneran hilang.
Aku pernah lihat junior analyst ngehapus 40.000 baris transaksi gara-gara nge-highlight query-nya kurang satu baris waktu tekan Run. Semenit doang.
Jadi artikel ini bukan cuma soal sintaks DELETE. Ini soal kebiasaan yang bikin kamu nggak pernah ngalamin itu.
DELETE adalah perintah SQL buat menghapus satu atau lebih baris dari sebuah tabel. Sintaks dasarnya DELETE FROM nama_tabel WHERE kondisi;, klausa WHERE nentuin baris mana yang kena. Kalau WHERE nggak ditulis, DELETE bakal menghapus semua baris di tabel, tapi struktur tabelnya (kolom, index, constraint) tetap utuh.
-- Hapus satu baris spesifik
DELETE FROM transaksi
WHERE id = 42;
-- Hapus banyak baris berdasarkan kondisi
DELETE FROM transaksi
WHERE status = 'batal';
-- ⚠️ BAHAYA: ngosongin seluruh tabel
DELETE FROM transaksi;
Yang terakhir itu valid secara sintaks. Database nggak bakal nanya "yakin?". Dia langsung jalan.
Ini kebiasaan paling murah yang bisa nyelamatin kamu.
Alurnya: tulis query-nya sebagai SELECT dulu. Lihat hasilnya. Kalau barisnya persis yang kamu maksud, baru ganti SELECT * jadi DELETE.
-- Langkah 1: SELECT dulu
SELECT * FROM transaksi
WHERE status = 'batal';
-- Lihat hasilnya: 3 baris? Sesuai ekspektasi? Oke.
-- Langkah 2: ganti SELECT * jadi DELETE
DELETE FROM transaksi
WHERE status = 'batal';
Bagian WHERE-nya nggak berubah sama sekali. Kamu cuma ganti kata di depan.
Kenapa ini penting? Karena kesalahan DELETE hampir selalu ada di WHERE-nya, bukan di kata DELETE-nya.
WHERE yang kamu kira nyaring 3 baris ternyata nyaring 3.000. SELECT dulu bikin kamu lihat itu sebelum datanya hilang.
Trik tambahan: kalau kamu belum kuat sama logika WHERE, baca dulu panduan klausa WHERE. Di situ aku bahas jebakan NULL yang sering bikin filter meleset.
Bungkus pakai transaction. Ini pengaman kedua kamu.
BEGIN;
DELETE FROM transaksi
WHERE tanggal < '2026-01-01';
-- Cek dulu: masuk akal nggak sisanya?
SELECT COUNT(*) FROM transaksi;
-- Kalau salah:
ROLLBACK;
-- Kalau benar:
COMMIT;
Antara BEGIN dan COMMIT, perubahan kamu belum permanen. ROLLBACK bakal balikin semuanya kayak semula.
Satu jebakan besar: banyak GUI SQL nyalain autocommit secara default. Artinya tiap query yang kamu Run langsung permanen, walaupun kamu ngetik BEGIN.
Cek setting autocommit di tool kamu sebelum ngerjain DELETE yang serius. Di DBeaver ada di toolbar atas. Di pgAdmin ada di menu Query.
PostgreSQL punya bonus yang enak: RETURNING.
DELETE FROM transaksi
WHERE status = 'batal'
RETURNING id, produk, total;
Dia ngasih tau persis baris apa yang barusan kehapus. Bukti buat kamu, bukan cuma angka "DELETE 3".
Ini kasus paling sering di kerjaan analis. Data dari POS masuk dobel gara-gara sync error.
Langkah 1: cari duplikatnya dulu.
SELECT pelanggan_id, tanggal, produk, COUNT(*) AS jumlah
FROM transaksi
GROUP BY pelanggan_id, tanggal, produk
HAVING COUNT(*) > 1;
Langkah 2: hapus duplikatnya, sisakan yang id-nya paling kecil.
DELETE FROM transaksi t
WHERE t.id > (
SELECT MIN(t2.id)
FROM transaksi t2
WHERE t2.pelanggan_id = t.pelanggan_id
AND t2.tanggal = t.tanggal
AND t2.produk = t.produk
);
Logikanya: buat tiap grup duplikat, cari id terkecil. Hapus semua yang id-nya lebih besar dari itu.
Yang bikin ini aman: baris pertama dari tiap grup selalu selamat. Kamu nggak akan kehilangan datanya, cuma salinannya.
Jalanin di dalam BEGIN dulu, ya. Percaya sama aku.
Dataset toko_berkah, data distributor sembako. Tabel transaksi punya 8.400 baris.
Sebelum bersih-bersih, aku hitung dulu apa yang mau dihapus:
SELECT status, COUNT(*) AS jumlah, SUM(total) AS nilai
FROM transaksi
GROUP BY status;
| Status | Jumlah baris | Nilai (Rp) |
|---|---|---|
| selesai | 7.611 | 4.820.000.000 |
| batal | 642 | 318.000.000 |
| pending | 147 | 91.000.000 |
Nah, ini yang bikin aku nggak jadi DELETE.
642 transaksi batal itu bernilai Rp318 juta. Kalau dihapus, tim finance kehilangan kemampuan buat jawab "kenapa Desember banyak yang batal?"
Angka rata-rata transaksi batal Rp495 ribu, sementara transaksi selesai Rp633 ribu. Transaksi batal cenderung yang kecil-kecil. Itu insight yang cuma ada kalau datanya masih di situ.
Jadi aku pakai soft delete. Data batal ditandai, nggak dibuang.
Ini pelajaran yang lebih penting dari sintaks DELETE-nya: tanya dulu "siapa yang bakal nyariin data ini enam bulan lagi" sebelum kamu tekan Run.
Soft delete = kamu nggak hapus barisnya, cuma nandain.
-- Tambah kolom penanda
ALTER TABLE pelanggan ADD COLUMN deleted_at TIMESTAMP NULL;
-- "Hapus" pelanggan id 5
UPDATE pelanggan
SET deleted_at = NOW()
WHERE id = 5;
-- Semua query berikutnya harus nyaring
SELECT * FROM pelanggan
WHERE deleted_at IS NULL;
| Situasi | Pilih |
|---|---|
| Data transaksi, pelanggan, order | Soft delete |
| Data yang kepake buat audit atau laporan historis | Soft delete |
| User bisa "undo" hapus | Soft delete |
| Data staging / tabel sementara | Hard delete |
| Log lama yang udah kadaluwarsa | Hard delete |
| Data pribadi yang harus dihapus permanen (GDPR/UU PDP) | Hard delete |
Harga soft delete: semua query kamu harus nambah WHERE deleted_at IS NULL. Lupa sekali, laporan kamu ngitung data yang harusnya udah nggak ada.
Solusinya bikin VIEW yang udah ada filternya, terus semua orang query dari VIEW itu.
Ini penyebab bencana nomor satu.
Kamu punya query dua baris:
DELETE FROM transaksi
WHERE status = 'batal';
Kamu blok pakai mouse, tapi kelewat baris kedua. Tekan Ctrl+Enter. Yang jalan cuma DELETE FROM transaksi.
Tabel ludes.
Kebiasaan pencegah: tulis WHERE-nya duluan. Ketik WHERE status = 'batal' di baris pertama, baru naik ke atas dan tambahin DELETE FROM transaksi. Query yang belum lengkap bakal error, bukan jalan setengah.
Setelah kamu hapus semua baris pakai DELETE, insert berikutnya tetap lanjut dari id terakhir. Kalau id terakhir 8.400, baris baru dapat id 8.401.
Mau balik ke 1? Itu kerjaan TRUNCATE. Aku bandingin dua-duanya di TRUNCATE vs DELETE.
ERROR: update or delete on table "pelanggan" violates
foreign key constraint on table "transaksi"
Artinya ada baris di tabel transaksi yang masih nunjuk ke pelanggan itu. Database nolak biar transaksinya nggak jadi yatim.
Tiga jalan keluar:
ON DELETE CASCADE, hati-hati, ini bikin hapus 1 pelanggan otomatis hapus semua transaksinyaDetail perilaku CASCADE ada di dokumentasi constraint PostgreSQL.
DELETE nulis log buat tiap baris. Hapus 10 juta baris sekaligus bisa bikin tabel kekunci berjam-jam dan disk penuh sama WAL.
Pecah jadi batch:
DELETE FROM log_lama
WHERE id IN (
SELECT id FROM log_lama
WHERE tanggal < '2024-01-01'
LIMIT 10000
);
-- ulangi sampai 0 baris kehapus
Nggak ada trik yang bisa nyelametin ini. Sebelum DELETE besar di production, pastiin ada backup yang bisa di-restore.
Kalau kamu ragu apakah backup-nya jalan, artinya kamu nggak punya backup.
Semua baris di tabel itu kehapus. DELETE FROM transaksi; ngosongin seluruh tabel, tanpa konfirmasi, tanpa undo. Kalau kamu belum jalan di dalam transaction dan nggak punya backup, datanya hilang beneran. Biasakan tulis WHERE-nya duluan, atau jalanin di dalam BEGIN supaya masih bisa ROLLBACK.
Cuma bisa kalau DELETE dijalanin di dalam transaction dan belum COMMIT. Ketik BEGIN; sebelum DELETE, cek jumlah baris yang kehapus, terus ROLLBACK; kalau salah. Kalau udah ke-commit atau kamu pakai autocommit, satu-satunya jalan pulih adalah restore dari backup.
DELETE menghapus baris dan bisa disaring pakai WHERE. TRUNCATE menghapus semua baris sekaligus, jauh lebih cepat, tapi nggak bisa pakai WHERE. DROP menghapus tabelnya sama sekali: baris, kolom, index, semuanya lenyap. Urutan risikonya: DELETE paling aman, DROP paling ganas.
Karena ada tabel lain yang masih nunjuk ke baris yang mau kamu hapus. Misal kamu hapus pelanggan, tapi tabel transaksi masih nyimpen customer_id-nya. Database nolak biar datanya nggak jadi yatim. Solusinya: hapus dulu baris anaknya, pakai ON DELETE CASCADE, atau pilih soft delete.
Kalau datanya masih mungkin dibutuhin buat audit, laporan historis, atau bisa dibatalin user. Tambah kolom deleted_at, terus UPDATE kolom itu alih-alih DELETE. Konsekuensinya semua query harus nambah filter WHERE deleted_at IS NULL. Buat data transaksi dan pelanggan, soft delete hampir selalu pilihan yang lebih waras.
Tiga kebiasaan yang bikin kamu nggak pernah kehilangan data:
Copy DDL dan query di artikel ini ke database latihan kamu, terus coba sendiri, termasuk yang ROLLBACK. Ngerasain sendiri gimana ROLLBACK nyelametin kamu itu jauh lebih nempel daripada baca.
Lanjut ke TRUNCATE vs DELETE buat tau kapan mending pakai yang lebih cepat.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Analisa per kuartal, hari kerja, atau musim jadi ribet kalau tiap query ngitung ulang atribut tanggal. Tabel kalender nyimpen semua atribut itu sekali, biar tinggal di-JOIN.
Laporan penjualan harian sering bolong di hari tanpa transaksi. Ini cara bikin deret tanggal lengkap di SQL biar tiap hari muncul, walau nilainya nol.
Struktur organisasi tersimpan sebagai kolom id_atasan yang saling nunjuk. Ini cara narik rantai jabatan, hitung total bawahan, dan span of control cuma pakai SQL.