Tabel staging yang harus kosong setiap pagi, tabel log yang dirotasi per bulan, data uji yang dibuang sebelum rilis — semua skenario ini punya kebutuhan yang sama: mengosongkan tabel seluruhnya. DELETE FROM tabel bisa melakukannya, tetapi pada tabel berjuta baris perintah itu memindai dan menandai setiap baris sebagai mati satu per satu. TRUNCATE menempuh jalur yang sama sekali berbeda: server tidak menyentuh satu baris pun dan tetap menyelesaikan pekerjaan dalam waktu yang nyaris konstan. Kekuatan sebesar itu datang dengan batas — tanpa WHERE, tanpa trigger per baris, dan dengan perlakuan khusus terhadap foreign key — semuanya kita uji langsung di chapter penutup section ini.

Menyiapkan Tabel Latihan #

Dua tabel bersahabat dari chapter DELETE kembali dipakai: produk sebagai induk dan pesanan sebagai anak yang menautkannya lewat foreign key, karena interaksi TRUNCATE dengan foreign key adalah bagian terpenting chapter ini:

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
);

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 count(*) AS jumlah_produk FROM produk;

SELECT count(*) AS jumlah_pesanan FROM pesanan;

Output yang Dihasilkan:

text
CREATE TABLE
CREATE TABLE
INSERT 0 5
INSERT 0 3
 jumlah_produk 
---------------
             5
(1 row)

 jumlah_pesanan 
----------------
              3
(1 row)

Lima produk dan tiga pesanan siap dikorbankan. Kita mulai dari tabel anak pesanan — tabel yang tidak direferensikan siapa-siapa sehingga bebas di-TRUNCATE:

TRUNCATE pesanan;

SELECT count(*) AS jumlah_pesanan FROM pesanan;

Output yang Dihasilkan:

text
TRUNCATE TABLE
 jumlah_pesanan 
----------------
              0
(1 row)

Tag outputnya khas: TRUNCATE TABLE, tanpa angka jumlah baris — server memang tidak menghitungnya. count(*) mengonfirmasi tabel kini kosong, tetapi struktur tabelnya sama sekali tidak tersentuh: kolom, primary key, index, dan constraint tetap utuh; perintah INSERT berikutnya akan berjalan normal. Yang hilang hanya isi, dan waktunya hampir tidak bertambah seberapa pun jumlah barisnya — bandingkan dengan DELETE FROM pesanan yang menghasilkan tag DELETE 3 dan bekerja sepanjang jumlah baris.

Mengapa TRUNCATE Secepat Itu #

Perbedaan kecepatan itu bukan sihir, melainkan dua mekanisme yang berbeda total. DELETE bekerja lewat jalur MVCC biasa: setiap baris dikunjungi, ditandai mati untuk transaction yang menjalankannya, dan jejaknya dibersihkan belakangan oleh VACUUM — ruang disknya pun tetap dikuasai tabel sampai proses itu selesai. TRUNCATE memotong masalahnya: server membuat berkas relasi kosong untuk menggantikan berkas berisi data, sehingga baris tidak pernah discan dan ruang disk langsung direklamasi ke sistem operasi seketika. Dokumentasi PostgreSQL menyebutnya lugas: TRUNCATE mengklaim kembali ruang disk segera, bukan menunggu VACUUM seperti DELETE.

AspekDELETE FROM tabelTRUNCATE tabel
CakupanSeluruh tabel atau yang lolos WHERESelalu seluruh tabel
Waktu eksekusiSebanding jumlah barisNyaris konstan, berapa pun isinya
Trigger level barisDijalankan tiap barisTidak dijalankan
RETURNINGDidukungTidak didukung
Ruang disk setelah operasiTetap dikuasai tabel, menunggu VACUUMLangsung direklamasi
Foreign keyDiperiksa per barisDiperiksa secara struktural
Dalam transactionAman, bisa di-ROLLBACKAman, bisa di-ROLLBACK

Dua baris tabel itu layak digarisbawahi. Pertama, trigger level baris ON DELETE tidak pernah menyala pada TRUNCATE — tabel yang bergantung pada trigger untuk mencatat audit penghapusan akan diam sepenuhnya. Kedua, RETURNING tidak tersedia; tidak ada data yang dikirim balik karena memang tidak ada baris yang diproses satu per satu.

Foreign Key: Menolak Mentah Tanpa CASCADE #

DELETE memeriksa foreign key saat memproses baris: penghapusan induk ditolak kalau masih ada baris anak yang menunjuk. TRUNCATE tidak memproses baris sama sekali, jadi pemeriksaannya struktural: tabel yang direferensikan foreign key mana pun tidak boleh di-TRUNCATE — meski tabel anaknya sedang kosong:

TRUNCATE produk;

Output yang Dihasilkan:

text
psql:truncate-fk-error.sql:1: ERROR:  cannot truncate a table referenced in a foreign key constraint
DETAIL:  Table "pesanan" references "produk".
HINT:  Truncate table "pesanan" at the same time, or use TRUNCATE ... CASCADE.

pesanan memang sudah kosong di langkah sebelumnya, tetapi error tetap keluar: cukup bahwa constraint-nya ada. Pesan HINT di error menawarkan dua jalan keluar resmi, dan keduanya valid. Jalan pertama: sebutkan kedua tabel sekaligus dalam satu perintah, TRUNCATE pesanan, produk — server mengosongkan anak dan induk bersamaan sehingga tidak ada jendela waktu referensi menggantung. Jalan kedua adalah CASCADE:

TRUNCATE produk CASCADE;

SELECT count(*) AS jumlah_produk FROM produk;

SELECT count(*) AS jumlah_pesanan FROM pesanan;

Output yang Dihasilkan:

text
TRUNCATE TABLE
 jumlah_produk 
---------------
             0
(1 row)

 jumlah_pesanan 
----------------
              0
(1 row)

CASCADE menyuruh PostgreSQL menelusuri semua tabel yang mereferensikan produk dan ikut mengosongkannya — di lingkungan kita hanya pesanan, di schema produksi bisa berantai ke banyak tabel anak. psql menampilkan NOTICE: truncate cascades to table "pesanan" di terminal (pesan ini ditulis ke stderr sehingga tidak ikut blok output di atas), dan kedua count(*) mengonfirmasi dua-duanya kosong. Berbeda dengan TRUNCATE pesanan, produk yang eksplisit, CASCADE bekerja diam-diam mengikuti struktur constraint — praktis untuk maintenance rutin, tetapi berbahaya di tangan yang belum hafal diagram relasinya; sebaiknya dipakai setelah memastikan daftar tabel yang akan tersapu.

RESTART IDENTITY: Mereset Penomoran Otomatis #

Tabel dengan identity column atau sequence melanjutkan penomoran setelah pengosongan — TRUNCATE tidak menyentuh penomorannya secara bawaan (CONTINUE IDENTITY adalah perilaku default). Opsi RESTART IDENTITY mengembalikan penghitung ke awal, dan demo ini membandingkan keduanya dalam satu file:

CREATE TABLE log_sesi (
    id       integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    pengguna text NOT NULL
);

INSERT INTO log_sesi (pengguna) VALUES ('budi'), ('siti'), ('wati');

SELECT id, pengguna FROM log_sesi ORDER BY id;

TRUNCATE log_sesi;

INSERT INTO log_sesi (pengguna) VALUES ('rani');

SELECT id, pengguna FROM log_sesi ORDER BY id;

TRUNCATE log_sesi RESTART IDENTITY;

INSERT INTO log_sesi (pengguna) VALUES ('dewi');

SELECT id, pengguna FROM log_sesi ORDER BY id;

Output yang Dihasilkan:

text
CREATE TABLE
INSERT 0 3
 id | pengguna 
----+----------
  1 | budi
  2 | siti
  3 | wati
(3 rows)

TRUNCATE TABLE
INSERT 0 1
 id | pengguna 
----+----------
  4 | rani
(1 row)

TRUNCATE TABLE
INSERT 0 1
 id | pengguna 
----+----------
  1 | dewi
(1 row)

Tiga babak di output: tiga baris pertama mendapat id 1–3; setelah TRUNCATE biasa, baris baru rani mendapat id 4 — penghitung melanjutkan seolah tidak pernah kosong; setelah TRUNCATE RESTART IDENTITY, baris dewi kembali mendapat id 1. Pilihan antara keduanya adalah keputusan desain: melanjutkan penomoran menghindari dua baris berbeda zaman membagi id yang sama (penting kalau id pernah tercatat di sistem lain), sedangkan mereset cocok untuk tabel yang benar-benar memulai hidup baru — tabel staging atau data uji yang dihapus sebelum rilis.

TRUNCATE Bersifat Transactional #

Banyak database lain (MySQL/MariaDB, Oracle) memperlakukan TRUNCATE sebagai perintah DDL yang melakukan implicit commit — sekali dijalankan, tidak bisa dibatalkan. PostgreSQL tidak: TRUNCATE adalah operasi MVCC biasa yang bisa dibungkus BEGIN ... ROLLBACK seperti INSERT atau UPDATE. Kita buktikan dengan mengisi ulang produk, mengosongkannya di dalam transaction, lalu membatalkan:

INSERT INTO produk (id, nama, kategori, harga) VALUES
    (1, 'Buku Tulis', 'alat tulis', 4500),
    (2, 'Pulpen Hitam', 'alat tulis', 3000);

BEGIN;

TRUNCATE produk CASCADE;

SELECT count(*) AS produk_di_dalam_transaksi FROM produk;

ROLLBACK;

SELECT count(*) AS produk_setelah_rollback FROM produk;

Output yang Dihasilkan:

text
INSERT 0 2
BEGIN
TRUNCATE TABLE
 produk_di_dalam_transaksi 
---------------------------
                         0
(1 row)

ROLLBACK
 produk_setelah_rollback 
-------------------------
                       2
(1 row)

Di dalam transaction, count(*) membaca nol — tabel memang sudah kosong bagi kita. Tapi ROLLBACK mengembalikan semuanya: dua baris yang kita sisipkan sebelum TRUNCATE tetap hidup setelah pembatalan. Perilaku ini membuat pola kerja berikut sah di PostgreSQL: bungkus TRUNCATE dalam BEGIN, jalankan pemuatan data baru di transaction yang sama, dan periksa hasilnya — hanya COMMIT yang menjadikan pengosongan permanen. Satu pengecualian teknis: seperti yang barusan kita lihat, TRUNCATE di dalam transaction tetap menjalankan CASCADE ke tabel-tabel anak saat itu juga, sehingga transaction yang menyapu banyak tabel perlu disusun dengan urutan yang hati-hati.

Dengan TRUNCATE, empat operasi dasar plus tiga fitur lanjutan section ini lengkap sudah: INSERT mengisi, SELECT membaca, UPDATE memperbaiki, DELETE dan TRUNCATE membersihkan, sementara RETURNING, ON CONFLICT, dan MERGE melengkapi keduanya dengan umpan balik dan logika bentrok. Perintah-perintah ini akan terus jadi bekal di seluruh course. Kebutuhan berikutnya yang langsung terasa di lapangan: cara memfilter baris yang kita mau baca — WHERE beserta keluarga operatornya — pembuka Section Filtering dan Sorting.

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
);

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 count(*) AS jumlah_produk FROM produk;

SELECT count(*) AS jumlah_pesanan FROM pesanan;

truncate-cascade.sql

Unduh berkas
sql
TRUNCATE produk CASCADE;

SELECT count(*) AS jumlah_produk FROM produk;

SELECT count(*) AS jumlah_pesanan FROM pesanan;

truncate-dasar.sql

Unduh berkas
sql
TRUNCATE pesanan;

SELECT count(*) AS jumlah_pesanan FROM pesanan;

truncate-restart-identity.sql

Unduh berkas
sql
CREATE TABLE log_sesi (
    id       integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    pengguna text NOT NULL
);

INSERT INTO log_sesi (pengguna) VALUES ('budi'), ('siti'), ('wati');

SELECT id, pengguna FROM log_sesi ORDER BY id;

TRUNCATE log_sesi;

INSERT INTO log_sesi (pengguna) VALUES ('rani');

SELECT id, pengguna FROM log_sesi ORDER BY id;

TRUNCATE log_sesi RESTART IDENTITY;

INSERT INTO log_sesi (pengguna) VALUES ('dewi');

SELECT id, pengguna FROM log_sesi ORDER BY id;

truncate-transaksional.sql

Unduh berkas
sql
INSERT INTO produk (id, nama, kategori, harga) VALUES
    (1, 'Buku Tulis', 'alat tulis', 4500),
    (2, 'Pulpen Hitam', 'alat tulis', 3000);

BEGIN;

TRUNCATE produk CASCADE;

SELECT count(*) AS produk_di_dalam_transaksi FROM produk;

ROLLBACK;

SELECT count(*) AS produk_setelah_rollback FROM produk;