Tim gudang Toko Maju Jaya mengimpor 500 produk baru dalam satu perintah INSERT sekaligus. Pertanyaan sederhana muncul: berapa kali trigger kita berjalan — sekali, atau lima ratus kali? Jawabannya bergantung pada satu kata di CREATE TRIGGER: FOR EACH ROW atau FOR EACH STATEMENT. Pilihan itu menentukan bukan hanya jumlah pemanggilan, tetapi juga variabel yang boleh disentuh: hanya trigger row-level yang punya akses ke record NEW dan OLD, sedangkan trigger statement-level justru unggul saat Anda butuh gambaran satu perintah secara utuh. Chapter ini mengukur kedua level dengan penghitung nyata, lalu melengkapi statement-level dengan dua senjata tambahan: kondisi WHEN dan transition table.
Dua Level, Dua Cara Melihat Perintah #
Trigger FOR EACH ROW menyala sekali untuk setiap baris yang terkena perintah: import 500 baris berarti 500 kali pemanggilan, dan setiap pemanggilan menerima record NEW (baris baru) atau OLD (baris lama) sebagai konteks. Trigger FOR EACH STATEMENT menyala tepat sekali untuk seluruh perintah, tidak peduli berapa baris yang tersentuh — nol baris pun tetap dihitung satu perintah. Karena satu panggilan mewakili banyak baris, record NEW dan OLD tidak tersedia di level ini; nilainya NULL.
Demo kita memakai tabel produk dan papan penghitung statistik_tulis yang menampung satu baris setiap kali sebuah trigger terpanggil:
-- Tabel utama dan papan penghitung pemanggilan trigger
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(12,2) NOT NULL
);
CREATE TABLE statistik_tulis (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tingkat text NOT NULL
);Output yang Dihasilkan:
CREATE TABLE
CREATE TABLEMengukur dengan Penghitung Nyata #
Dua trigger dipasang pada event yang sama — AFTER INSERT ON produk — tetapi dengan level berbeda. Function pertama mencatat ROW tiap kali dipanggil, function kedua mencatat STATEMENT:
-- Dua trigger pada event yang sama: satu per baris, satu per pernyataan
CREATE FUNCTION catat_baris() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO statistik_tulis (tingkat) VALUES ('ROW');
RETURN NEW;
END
$$;
CREATE FUNCTION catat_pernyataan() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO statistik_tulis (tingkat) VALUES ('STATEMENT');
RETURN NULL;
END
$$;
CREATE TRIGGER hitung_setiap_baris
AFTER INSERT ON produk
FOR EACH ROW EXECUTE FUNCTION catat_baris();
CREATE TRIGGER hitung_setiap_pernyataan
AFTER INSERT ON produk
FOR EACH STATEMENT EXECUTE FUNCTION catat_pernyataan();
-- SATU perintah INSERT dengan TIGA baris
INSERT INTO produk (id, nama, harga) VALUES
(1, 'Teh Melati', 12000.00),
(2, 'Kopi Kapal', 18000.00),
(3, 'Gula Pasir', 14000.00);
SELECT tingkat, count(*) AS pemanggilan
FROM statistik_tulis
GROUP BY tingkat
ORDER BY tingkat;Output yang Dihasilkan:
CREATE FUNCTION
CREATE FUNCTION
CREATE TRIGGER
CREATE TRIGGER
INSERT 0 3
tingkat | pemanggilan
-----------+-------------
ROW | 3
STATEMENT | 1
(2 rows)Satu perintah INSERT 0 3, dua hasil berbeda: ROW terpanggil 3 kali (sekali per baris: Teh Melati, Kopi Kapal, Gula Pasir), STATEMENT terpanggil 1 kali (sekali untuk seluruh perintah). Angka itu adalah kontrak kedua level — kalau import gudang berisi 500 baris, function catat_baris() akan bekerja 500 kali, sementara catat_pernyataan() tetap bekerja sekali.
Pernyataan Kosong Tetap Menyalakan Statement Trigger #
Konsekuensi menarik dari “sekali per perintah”: perintah yang tidak mengubah satu baris pun tetap dihitung. Uji berikut menjalankan INSERT ... SELECT dengan kondisi WHERE false, sehingga tidak ada baris yang masuk:
-- INSERT yang tidak memasukkan baris apa pun: statement trigger tetap menyala
INSERT INTO produk (id, nama, harga)
SELECT 99, 'Produk Hantu', 1.00
WHERE false;
SELECT tingkat, count(*) AS pemanggilan
FROM statistik_tulis
GROUP BY tingkat
ORDER BY tingkat;Output yang Dihasilkan:
INSERT 0 0
tingkat | pemanggilan
-----------+-------------
ROW | 3
STATEMENT | 2
(2 rows)Tag perintahnya INSERT 0 0 — nol baris — tetapi penghitung STATEMENT naik dari 1 menjadi 2, sementara ROW mentok di 3. Trigger row-level diam karena tidak ada baris untuk diproses; trigger statement-level tetap bersuara karena perintahnya benar-benar dieksekusi. Perilaku ini penting saat function Anda memelihara agregat: statement-level memicu penyegaran walau tidak ada data baru.
WHEN: Menyaring Baris Sebelum Function Berjalan #
Trigger row-level bisa diberi kondisi WHEN langsung di CREATE TRIGGER. Server mengevaluasi kondisi itu untuk setiap baris, dan function hanya dipanggil bila kondisinya terpenuhi — baris yang tidak lolos tidak membayar biaya pemanggilan sama sekali. Aturan bisnisnya: produk dengan harga di atas Rp10.000 ikut disalin ke tabel produk_mahal:
-- WHEN pada trigger row-level: hanya baris yang lolos syarat yang memicu function
CREATE TABLE produk_mahal (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(12,2) NOT NULL
);
CREATE FUNCTION salin_produk_mahal() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO produk_mahal (id, nama, harga)
VALUES (NEW.id, NEW.nama, NEW.harga);
RETURN NEW;
END
$$;
CREATE TRIGGER salin_mahal_insert
AFTER INSERT ON produk
FOR EACH ROW
WHEN (NEW.harga > 10000.00)
EXECUTE FUNCTION salin_produk_mahal();
INSERT INTO produk (id, nama, harga) VALUES
(4, 'Sabun Mandi', 8500.00),
(5, 'Sarden Kaleng', 25000.00);
SELECT id, nama, harga FROM produk_mahal ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
CREATE FUNCTION
CREATE TRIGGER
INSERT 0 2
id | nama | harga
----+---------------+----------
5 | Sarden Kaleng | 25000.00
(1 row)Dari dua baris yang masuk, hanya Sarden Kaleng yang tersalin — Sabun Mandi seharga Rp8.500 gagal lolos WHEN (NEW.harga > 10000.00), dan function salin_produk_mahal() tidak pernah terpanggil untuknya. Kondisi WHEN boleh membaca kolom NEW (untuk INSERT/UPDATE) dan OLD (untuk UPDATE/DELETE), memakai fungsi immutable seperti lower() atau date_trunc() — tetapi hanya pada trigger row-level. Statement-level tidak punya baris untuk dievaluasi:
-- WHEN pada statement-level trigger tidak boleh menyentuh kolom baris
CREATE FUNCTION tanpa_aksi() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
RETURN NULL;
END
$$;
CREATE TRIGGER when_terlarang
AFTER INSERT ON produk
FOR EACH STATEMENT
WHEN (NEW.harga > 0)
EXECUTE FUNCTION tanpa_aksi();Output yang Dihasilkan:
psql:when-statement-gagal.sql:14: ERROR: statement trigger's WHEN condition cannot reference column values
LINE 4: WHEN (NEW.harga > 0)
^Server menolak pembuatannya dengan statement trigger's WHEN condition cannot reference column values — bukan karena harga tidak ada di tabel, melainkan karena perintah satu-panggilan tidak memiliki satu baris tertentu yang bisa disebut NEW.
Transition Table: Jendela ke Semua Baris #
Level statement-level kehilangan NEW/OLD, tetapi sejak PostgreSQL 10 ia memperoleh pengganti yang jauh lebih luas: transition table. Klausa REFERENCING memberi nama pada himpunan seluruh baris yang terkena perintah, dan function memperlakukannya seperti tabel sementara biasa yang bisa di-SELECT, di-agregasi, di-join. Untuk INSERT dan UPDATE tersedia NEW TABLE, untuk UPDATE dan DELETE tersedia OLD TABLE:
-- Transition table: statement trigger membaca seluruh baris yang terkena perintah
CREATE TABLE log_batch (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
ringkas text NOT NULL
);
CREATE FUNCTION ringkas_batch() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_jumlah integer;
v_total numeric(14,2);
BEGIN
SELECT count(*), sum(harga)
INTO v_jumlah, v_total
FROM baris_baru;
INSERT INTO log_batch (ringkas)
VALUES ('batch masuk: ' || v_jumlah || ' baris, total ' || v_total);
RETURN NULL;
END
$$;
CREATE TRIGGER ringkas_batch_insert
AFTER INSERT ON produk
REFERENCING NEW TABLE AS baris_baru
FOR EACH STATEMENT EXECUTE FUNCTION ringkas_batch();
INSERT INTO produk (id, nama, harga) VALUES
(6, 'Susu Kotak', 6500.00),
(7, 'Milo Kaleng', 95000.00);
SELECT id, ringkas FROM log_batch ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
CREATE FUNCTION
CREATE TRIGGER
INSERT 0 2
id | ringkas
----+---------------------------------------
1 | batch masuk: 2 baris, total 101500.00
(1 row)Satu panggilan function menghasilkan ringkasan lengkap: batch masuk: 2 baris, total 101500.00 — penghitungan yang di level row-level harus dilakukan manual dengan penghitung tambahan, di sini selesai dalam satu query SELECT count(*), sum(harga) FROM baris_baru. Nama baris_baru bukan variabel PL/pgSQL, melainkan nama relasi yang berlaku di dalam badan function selama panggilan itu berlangsung.
Memilih Level #
| Aspek | FOR EACH ROW | FOR EACH STATEMENT |
|---|---|---|
| Frekuensi pemanggilan | sekali per baris tersentuh | sekali per perintah |
Record NEW/OLD | tersedia | NULL, tidak bisa dipakai |
Kondisi WHEN | boleh membaca NEW/OLD | boleh, tanpa menyentuh NEW/OLD |
| Transition table | sah, tapi berlebihan (ada NEW/OLD) | kegunaan utamanya: REFERENCING |
| Pemicu pada 0 baris | tidak menyala | tetap menyala |
| Biaya pada bulk besar | linear terhadap jumlah baris | konstan per perintah |
| Kegunaan khas | validasi/audit per baris | ringkasan, sinkronisasi massal |
Level dan timing sudah tuntas; satu pertanyaan masih menggantung. Function catat_baris() tahu banyak hal tentang pemanggilnya — baris NEW, tingkat, waktu — tetapi dari mana semua variabel itu datang, dan apa lagi yang tersedia? Chapter berikutnya, Trigger Function
, membedah isi function trigger baris demi baris: variabel TG_OP, TG_TABLE_NAME, TG_ARGV, hingga pola satu function yang dipakai banyak tabel.
Referensi #
- Overview of Trigger Behavior
- CREATE TRIGGER (WHEN dan REFERENCING)
- Trigger Functions (contoh transition table)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- Tabel utama dan papan penghitung pemanggilan trigger
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(12,2) NOT NULL
);
CREATE TABLE statistik_tulis (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tingkat text NOT NULL
);
hitung-pemanggilan.sql
Unduh berkas-- Dua trigger pada event yang sama: satu per baris, satu per pernyataan
CREATE FUNCTION catat_baris() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO statistik_tulis (tingkat) VALUES ('ROW');
RETURN NEW;
END
$$;
CREATE FUNCTION catat_pernyataan() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO statistik_tulis (tingkat) VALUES ('STATEMENT');
RETURN NULL;
END
$$;
CREATE TRIGGER hitung_setiap_baris
AFTER INSERT ON produk
FOR EACH ROW EXECUTE FUNCTION catat_baris();
CREATE TRIGGER hitung_setiap_pernyataan
AFTER INSERT ON produk
FOR EACH STATEMENT EXECUTE FUNCTION catat_pernyataan();
-- SATU perintah INSERT dengan TIGA baris
INSERT INTO produk (id, nama, harga) VALUES
(1, 'Teh Melati', 12000.00),
(2, 'Kopi Kapal', 18000.00),
(3, 'Gula Pasir', 14000.00);
SELECT tingkat, count(*) AS pemanggilan
FROM statistik_tulis
GROUP BY tingkat
ORDER BY tingkat;
insert-nol-baris.sql
Unduh berkas-- INSERT yang tidak memasukkan baris apa pun: statement trigger tetap menyala
INSERT INTO produk (id, nama, harga)
SELECT 99, 'Produk Hantu', 1.00
WHERE false;
SELECT tingkat, count(*) AS pemanggilan
FROM statistik_tulis
GROUP BY tingkat
ORDER BY tingkat;
transition-table.sql
Unduh berkas-- Transition table: statement trigger membaca seluruh baris yang terkena perintah
CREATE TABLE log_batch (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
ringkas text NOT NULL
);
CREATE FUNCTION ringkas_batch() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_jumlah integer;
v_total numeric(14,2);
BEGIN
SELECT count(*), sum(harga)
INTO v_jumlah, v_total
FROM baris_baru;
INSERT INTO log_batch (ringkas)
VALUES ('batch masuk: ' || v_jumlah || ' baris, total ' || v_total);
RETURN NULL;
END
$$;
CREATE TRIGGER ringkas_batch_insert
AFTER INSERT ON produk
REFERENCING NEW TABLE AS baris_baru
FOR EACH STATEMENT EXECUTE FUNCTION ringkas_batch();
INSERT INTO produk (id, nama, harga) VALUES
(6, 'Susu Kotak', 6500.00),
(7, 'Milo Kaleng', 95000.00);
SELECT id, ringkas FROM log_batch ORDER BY id;
when-kondisi.sql
Unduh berkas-- WHEN pada trigger row-level: hanya baris yang lolos syarat yang memicu function
CREATE TABLE produk_mahal (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(12,2) NOT NULL
);
CREATE FUNCTION salin_produk_mahal() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO produk_mahal (id, nama, harga)
VALUES (NEW.id, NEW.nama, NEW.harga);
RETURN NEW;
END
$$;
CREATE TRIGGER salin_mahal_insert
AFTER INSERT ON produk
FOR EACH ROW
WHEN (NEW.harga > 10000.00)
EXECUTE FUNCTION salin_produk_mahal();
INSERT INTO produk (id, nama, harga) VALUES
(4, 'Sabun Mandi', 8500.00),
(5, 'Sarden Kaleng', 25000.00);
SELECT id, nama, harga FROM produk_mahal ORDER BY id;
when-statement-gagal.sql
Unduh berkas-- WHEN pada statement-level trigger tidak boleh menyentuh kolom baris
CREATE FUNCTION tanpa_aksi() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
RETURN NULL;
END
$$;
CREATE TRIGGER when_terlarang
AFTER INSERT ON produk
FOR EACH STATEMENT
WHEN (NEW.harga > 0)
EXECUTE FUNCTION tanpa_aksi();