Tidak semua data boleh hidup selamanya: pesanan yang dibatalkan, produk yang sudah tidak dijual, log yang kedaluwarsa. Perintah DELETE mengeluarkan baris dari tabel secara permanen — dan karena permanen, ia satu keluarga bahaya dengan UPDATE: lupa klausa WHERE berarti mengosongkan seluruh tabel dalam satu tarikan napas. Chapter ini melatih penghapusan yang presisi, lalu menghadapi dua kenyataan penting di dunia nyata: foreign key yang menolak menghapus data yang masih dipakai tabel lain, serta perbedaan mendasar antara DELETE dan TRUNCATE yang kerap disalahpahami.

Menyiapkan Tabel Latihan #

Penghapusan yang menarik selalu melibatkan lebih dari satu tabel, jadi kita siapkan dua tabel yang saling terhubung: produk sebagai tabel induk dan pesanan sebagai tabel anak yang mereferensikan produk.id lewat foreign key:

sql buat-tabel.sql
CREATE TABLE produk (
    id       integer PRIMARY KEY,
    nama     text NOT NULL,
    kategori text NOT NULL,
    harga    numeric(10, 2) NOT NULL
);

CREATE TABLE pesanan (
    id        integer PRIMARY KEY,
    produk_id integer NOT NULL REFERENCES produk(id),
    qty       integer NOT NULL CHECK (qty > 0)
);

INSERT INTO produk (id, nama, kategori, harga) VALUES
    (1, 'Buku Tulis', 'alat tulis', 4500),
    (2, 'Pulpen Hitam', 'alat tulis', 3000),
    (3, 'Kertas HVS A4', 'kertas', 55000),
    (4, 'Map Plastik', 'alat tulis', 12000),
    (5, 'Amplop Coklat', 'kertas', 1500);

INSERT INTO pesanan (id, produk_id, qty) VALUES
    (1001, 1, 2),
    (1002, 3, 1),
    (1003, 3, 5);

SELECT id, nama FROM produk ORDER BY id;

SELECT id, produk_id, qty FROM pesanan ORDER BY id;

Output yang Dihasilkan:

text
CREATE TABLE
CREATE TABLE
INSERT 0 5
INSERT 0 3
 id |     nama      
----+---------------
  1 | Buku Tulis
  2 | Pulpen Hitam
  3 | Kertas HVS A4
  4 | Map Plastik
  5 | Amplop Coklat
(5 rows)

  id  | produk_id | qty 
------+-----------+-----
 1001 |         1 |   2
 1002 |         3 |   1
 1003 |         3 |   5
(3 rows)

Klausa REFERENCES produk(id) pada kolom produk_id adalah janji kepada database: setiap nilai di kolom itu harus menunjuk baris produk yang ada. Janji ini akan diuji keras di bagian berikutnya.

Menghapus Baris Tertentu dengan WHERE #

DELETE mencari baris yang cocok dengan kondisi WHERE, lalu mengeluarkan semuanya dari tabel. Pesanan 1002 dibatalkan pembelinya, jadi baris itu kita hapus:

DELETE FROM pesanan WHERE id = 1002;

SELECT id, produk_id, qty FROM pesanan ORDER BY id;

Output yang Dihasilkan:

text
DELETE 1
  id  | produk_id | qty 
------+-----------+-----
 1001 |         1 |   2
 1003 |         3 |   5
(2 rows)

Tag DELETE 1 melaporkan satu baris hilang, dan SELECT sesudahnya mengonfirmasi sisa dua pesanan — 1001 dan 1003 — masih utuh. Sama seperti UPDATE, jumlah pada tag ini adalah satu-satunya umpan balik jumlah korban; angka yang tidak sesuai niat adalah sinyal kondisi WHERE kita keliruan. Perhatikan juga sintaksnya: DELETE FROM nama_tabel — kata FROM wajib ada, berbeda dengan UPDATE yang justru tidak memakai FROM pada bentuk dasarnya.

Kondisi WHERE pada DELETE sama kuatnya dengan WHERE pada SELECT: perbandingan kolom, IN, BETWEEN, sampai subquery yang menghapus baris berdasarkan data tabel lain. Seluruh perangkat itu dibahas tuntas di Section Filtering dan Sorting; untuk latihan ini kondisi sederhana sudah cukup memperlihatkan mekanismenya.

Foreign Key Menolak Penghapusan Induk #

Sekarang bagian yang menjengkelkan sekaligus menyelamatkan: Buku Tulis (id 1) sudah tidak dijual dan ingin dihapus dari katalog. Sayangnya, pesanan 1001 masih menunjuk produk itu:

DELETE FROM produk WHERE id = 1;

Output yang Dihasilkan:

text
psql:delete-fk-error.sql:1: ERROR:  update or delete on table "produk" violates foreign key constraint "pesanan_produk_id_fkey" on table "pesanan"
DETAIL:  Key (id)=(1) is still referenced from table "pesanan".

PostgreSQL menolak perintahnya mentah-mentah: pesanan 1001 akan menjadi yatim — baris yang menunjuk produk yang tidak ada. Pesan error bahkan memberi tahu pasangan yang bermasalah (Key (id)=(1)) dan tabel yang masih mereferensikannya (pesanan). Tidak ada baris produk yang terhapus; Buku Tulis masih ada di tabel.

Ada dua jalan keluar yang sah. Jalan pertama adalah yang akan kita praktikkan sebentar lagi: hapus atau pindahkan dulu baris anak yang menghalangi. Jalan kedua adalah mendefinisikan ulang foreign key dengan aksi otomatis — ON DELETE CASCADE ikut menghapus baris anak, atau ON DELETE SET NULL mengosongkan kolom referensinya — topik yang dibahas tuntas di chapter Foreign Key pada Section Table Design. Pilihan aksi adalah keputusan bisnis: apakah riwayat pesanan harus selamat dari penghapusan produk? Kalau ya, RESTRICT bawaan seperti inilah yang kita mau.

Menghapus Semua Baris dari Tabel #

Bentuk DELETE FROM nama_tabel tanpa WHERE adalah pernyataan yang sah dan kadang memang disengaja — misalnya membersihkan tabel staging setelah proses selesai. Kita gunakan untuk melepas blokiran foreign key di atas: kosongkan seluruh pesanan, lalu hapus produk yang tadi terhalang:

DELETE FROM pesanan;

SELECT count(*) AS pesanan_tersisa FROM pesanan;

DELETE FROM produk WHERE id = 1;

SELECT id, nama FROM produk ORDER BY id;

Output yang Dihasilkan:

text
DELETE 2
 pesanan_tersisa 
-----------------
               0
(1 row)

DELETE 1
 id |     nama      
----+---------------
  2 | Pulpen Hitam
  3 | Kertas HVS A4
  4 | Map Plastik
  5 | Amplop Coklat
(4 rows)

Cerita lengkapnya ada di empat tag output tersebut: DELETE 2 mengosongkan pesanan (dua baris tersisa saat itu), count(*) mengonfirmasi nol baris, DELETE 1 kini berhasil menghapus Buku Tulis karena tidak ada lagi yang menunjuknya, dan SELECT terakhir menunjukkan katalog tinggal empat produk. Urutan penghapusan anak-dulu-induk-kedua adalah pola standar ketika foreign key memakai perilaku bawaan.

Sekilas DELETE versus TRUNCATE #

Mengosongkan tabel lewat DELETE FROM tabel terasa cepat pada tabel kecil, tetapi mekanismenya berat untuk tabel besar: setiap baris dihapus satu per satu, memicu seluruh mekanisme MVCC, memanggil trigger ON DELETE bila ada, dan tetap melalui pemeriksaan foreign key. PostgreSQL menyediakan jalur khusus untuk kebutuhan ini:

AspekDELETE FROM tabelTRUNCATE tabel
CakupanSemua baris atau yang lolos WHERESelalu seluruh tabel, tanpa WHERE
MekanismeBaris demi baris (MVCC, trigger)Mengganti storage tabel sekaligus
Foreign keyDiperiksa per baris, menolak bila direferensikanMenolak bila tabel direferensikan (tanpa CASCADE)
Pengembalian ruangRuang tetap dikuasai tabel; menunggu VACUUMLangsung dikembalikan seketika

TRUNCATE bukan pengganti DELETE untuk penghapusan selektif — TRUNCATE tidak mengenal WHERE sama sekali. Batas tegas keduanya, opsi RESTART IDENTITY dan CASCADE, sampai fakta bahwa TRUNCATE di PostgreSQL bersifat transactional, semua dibedah di chapter TRUNCATE TABLE di ujung section ini.

Penghapusan yang barusan kita lakukan meninggalkan satu ketidaknyamanan praktis: kita baru tahu baris apa yang hilang setelah menjalankan SELECT lagi. Padahal PostgreSQL bisa membalikkan data yang dihapus langsung pada saat penghapusan terjadi — lewat klausa RETURNING, chapter berikutnya.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

buat-tabel.sql

Unduh berkas
sql
CREATE TABLE produk (
    id       integer PRIMARY KEY,
    nama     text NOT NULL,
    kategori text NOT NULL,
    harga    numeric(10, 2) NOT NULL
);

CREATE TABLE pesanan (
    id        integer PRIMARY KEY,
    produk_id integer NOT NULL REFERENCES produk(id),
    qty       integer NOT NULL CHECK (qty > 0)
);

INSERT INTO produk (id, nama, kategori, harga) VALUES
    (1, 'Buku Tulis', 'alat tulis', 4500),
    (2, 'Pulpen Hitam', 'alat tulis', 3000),
    (3, 'Kertas HVS A4', 'kertas', 55000),
    (4, 'Map Plastik', 'alat tulis', 12000),
    (5, 'Amplop Coklat', 'kertas', 1500);

INSERT INTO pesanan (id, produk_id, qty) VALUES
    (1001, 1, 2),
    (1002, 3, 1),
    (1003, 3, 5);

SELECT id, nama FROM produk ORDER BY id;

SELECT id, produk_id, qty FROM pesanan ORDER BY id;

delete-fk-error.sql

Unduh berkas
sql
DELETE FROM produk WHERE id = 1;

delete-semua.sql

Unduh berkas
sql
DELETE FROM pesanan;

SELECT count(*) AS pesanan_tersisa FROM pesanan;

DELETE FROM produk WHERE id = 1;

SELECT id, nama FROM produk ORDER BY id;

delete-where.sql

Unduh berkas
sql
DELETE FROM pesanan WHERE id = 1002;

SELECT id, produk_id, qty FROM pesanan ORDER BY id;