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:

sql buat-tabel.sql
-- 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:

text
CREATE TABLE
CREATE TABLE

Mengukur 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:

text
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:

text
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:

text
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:

text
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:

text
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 #

AspekFOR EACH ROWFOR EACH STATEMENT
Frekuensi pemanggilansekali per baris tersentuhsekali per perintah
Record NEW/OLDtersediaNULL, tidak bisa dipakai
Kondisi WHENboleh membaca NEW/OLDboleh, tanpa menyentuh NEW/OLD
Transition tablesah, tapi berlebihan (ada NEW/OLD)kegunaan utamanya: REFERENCING
Pemicu pada 0 baristidak menyalatetap menyala
Biaya pada bulk besarlinear terhadap jumlah bariskonstan per perintah
Kegunaan khasvalidasi/audit per barisringkasan, 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 #

Berkas Sumber

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

buat-tabel.sql

Unduh berkas
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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();