Function-function kita sejauh ini berjalan lurus: statement demi statement dari BEGIN sampai END. Padahal aturan bisnis Toko Maju Jaya penuh keputusan — diskon emas 15%, perak 10%, sisanya 5% — dan pengulangan — cicilan dipotong terus sampai sisa habis, tabungan tumbuh bulan demi bulan sampai mencapai target. PL/pgSQL menyediakan kendali alur lengkap untuk itu: percabangan IF dan CASE, perulangan LOOP, WHILE, FOR, serta FOREACH untuk array. Chapter ini mempraktikkan semuanya lewat satu domain yang sama, ditutup demo yang sengaja salah: loop tanpa pintu keluar yang dibatalkan statement_timeout — pengingat kenapa pola counter guard wajib menjadi refleks.
IF, ELSIF, ELSE #
IF mengevaluasi kondisi boolean dan menjalankan cabang yang cocok. Bentuk bertingkatnya memakai ELSIF (satu kata, tanpa E), dan ELSE menampung sisa:
-- IF / ELSIF / ELSE: diskon bertingkat berdasarkan level member
CREATE TABLE pelanggan (
id integer PRIMARY KEY,
nama text NOT NULL,
level text NOT NULL,
poin integer NOT NULL DEFAULT 0,
belanja numeric(12,2) NOT NULL DEFAULT 0
);
INSERT INTO pelanggan (id, nama, level, poin, belanja) VALUES
(1, 'Budi Santoso', 'gold', 1200, 850000.00),
(2, 'Siti Rahayu', 'silver', 450, 320000.00),
(3, 'Ani Kartika', 'biasa', 80, 95000.00);
CREATE FUNCTION diskon_member(level_member text, total_belanja numeric)
RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
v_diskon numeric;
BEGIN
IF level_member = 'gold' THEN
v_diskon := total_belanja * 0.15;
ELSIF level_member = 'silver' THEN
v_diskon := total_belanja * 0.10;
ELSE
v_diskon := total_belanja * 0.05;
END IF;
RETURN round(v_diskon, 2);
END
$$;
SELECT nama, level, belanja,
diskon_member(level, belanja) AS diskon
FROM pelanggan
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 3
CREATE FUNCTION
nama | level | belanja | diskon
--------------+--------+-----------+-----------
Budi Santoso | gold | 850000.00 | 127500.00
Siti Rahayu | silver | 320000.00 | 32000.00
Ani Kartika | biasa | 95000.00 | 4750.00
(3 rows)Bedah baris pentingnya:
IF level_member = 'gold' THEN ... ELSIF ... ELSE ... END IF;— kondisi dicek berurutan dari atas; begitu satu cabang cocok, sisanya dilewati. TanpaELSEdan tanpa kondisi yang cocok, eksekusi langsung melompat keEND IF.- Parameter
level_membersengaja tidak bernamalevelsupaya tidak bertabrakan dengan kolomleveltabelpelanggan— tabrakan nama parameter dengan kolom menghasilkan error ambigu yang sudah kita temui di chapter Return Type. - Pemanggilan
diskon_member(level, belanja)mengirim kolom sebagai argumen; keputusan di dalam function bekerja per baris: Budi127500.00, Siti32000.00, Ani4750.00.
Penulisan ELSEIF (ejaan alternatif) juga diterima PL/pgSQL, tetapi ELSIF adalah bentuk yang dipakai dokumentasi resmi. Untuk percabangan bertingkat yang dalam, blok IF juga bisa bersarang di dalam blok IF lain.
CASE Statement: Searched dan Simple #
PL/pgSQL punya CASE versi statement — berdiri sendiri sebagai perintah, bukan ekspresi di dalam query. Dua bentuknya:
-- CASE statement: bentuk searched dan bentuk simple
CREATE FUNCTION kategori_pelanggan(total_belanja numeric)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_label text;
BEGIN
-- Searched CASE: kondisi bebas, dicek berurutan dari atas
CASE
WHEN total_belanja >= 500000 THEN v_label := 'prioritas';
WHEN total_belanja >= 200000 THEN v_label := 'loyal';
ELSE v_label := 'baru';
END CASE;
RETURN v_label;
END
$$;
CREATE FUNCTION label_level(level_member text)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_label text;
BEGIN
-- Simple CASE: satu nilai dibandingkan terhadap daftar kandidat
CASE level_member
WHEN 'gold' THEN v_label := 'Anggota Emas';
WHEN 'silver' THEN v_label := 'Anggota Perak';
ELSE v_label := 'Anggota Biasa';
END CASE;
RETURN v_label;
END
$$;
SELECT kategori_pelanggan(850000) AS k1,
kategori_pelanggan(320000) AS k2,
kategori_pelanggan(95000) AS k3;
SELECT label_level('gold') AS l1,
label_level('silver') AS l2,
label_level('biasa') AS l3;Output yang Dihasilkan:
CREATE FUNCTION
CREATE FUNCTION
k1 | k2 | k3
-----------+-------+------
prioritas | loyal | baru
(1 row)
l1 | l2 | l3
--------------+---------------+---------------
Anggota Emas | Anggota Perak | Anggota Biasa
(1 row)- Searched CASE (
CASE WHEN kondisi THEN ...): kondisinya ekspresi boolean bebas, dievaluasi berurutan — pasangan alami untuk rentang nilai sepertitotal_belanja >= 500000. - Simple CASE (
CASE nilai WHEN kandidat THEN ...): satu nilai dibandingkan terhadap daftar kandidat — pas untuk pemetaan kode ke label sepertigold→Anggota Emas.
Satu jebakan nyata dari simple CASE tanpa ELSE: ketika tidak ada kandidat yang cocok, PL/pgSQL melempar error case not found lengkap dengan petunjuk CASE statement is missing ELSE part. — berbeda dari IF tanpa ELSE yang diam-diam lolos. Selalu tulis ELSE pada CASE yang inputnya tidak terjamin lengkap.
LOOP, EXIT, dan CONTINUE #
LOOP adalah perulangan tanpa kondisi apa pun — ia berputar selamanya sampai Anda menyuruhnya berhenti. Pintu keluarnya EXIT, dan CONTINUE memotong satu putaran:
-- LOOP tanpa kondisi, dikendalikan EXIT WHEN dan CONTINUE WHEN
CREATE FUNCTION hitung_cicilan(total numeric, maks_cicilan integer)
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
v_cicilan_ke integer := 0;
v_sisa numeric := total;
BEGIN
LOOP
v_cicilan_ke := v_cicilan_ke + 1;
v_sisa := v_sisa - 200000;
-- Dua pintu keluar: sisa habis, atau guard memutuskan cukup
EXIT WHEN v_sisa <= 0 OR v_cicilan_ke >= maks_cicilan;
END LOOP;
RETURN v_cicilan_ke;
END
$$;
CREATE FUNCTION hitung_positif(angka numeric[])
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
v_a numeric;
v_ada integer := 0;
BEGIN
FOREACH v_a IN ARRAY angka LOOP
CONTINUE WHEN v_a < 0;
v_ada := v_ada + 1;
END LOOP;
RETURN v_ada;
END
$$;
SELECT hitung_cicilan(350000, 12) AS ringan,
hitung_cicilan(1500000, 5) AS dihentikan_guard;
SELECT hitung_positif(ARRAY[10, -5, 20, -1, 30]::numeric[]) AS jumlah_positif;Output yang Dihasilkan:
CREATE FUNCTION
CREATE FUNCTION
ringan | dihentikan_guard
--------+------------------
2 | 5
(1 row)
jumlah_positif
----------------
3
(1 row)LOOP ... EXIT WHEN v_sisa <= 0 OR v_cicilan_ke >= maks_cicilan; END LOOP;— functionhitung_cicilanmemotong Rp200.000 tiap putaran. KondisiEXIT WHENmemuat dua syarat: sisa habis secara alami, atau counter mencapai batasmaks_cicilan.- Pemanggilan kedua membuktikan guard bekerja: total Rp1.500.000 sebenarnya butuh 8 putaran, tetapi
maks_cicilan = 5menghentikan loop di putaran kelima — mengembalikan5, bukan8. Inilah pola counter guard: batas atas yang pasti dicapai, apa pun kondisi data. CONTINUE WHEN v_a < 0;— melempar putaran berisi nilai negatif ke putaran berikutnya tanpa mengeksekusi sisa badan loop; dari lima elemen array, tiga dihitung.- Bentuk polos
EXIT;danCONTINUE;(tanpaWHEN) juga sah — biasanya dipakai di dalamIFbersarang.EXIT/CONTINUEdengan label blok dapat menargetkan loop luar dari loop bersarang.
Loop Tanpa Pintu Keluar dan statement_timeout #
Bagaimana bila guard lupa ditulis? Demo berikut sengaja menjalankan WHILE true tanpa perintah keluar, dengan satu jaring pengaman: statement_timeout sesi yang membatalkan statement setelah 200 milidetik:
-- Loop tanpa pintu keluar: jaring pengaman statement_timeout yang membatalkannya
SET statement_timeout = '200ms';
DO $$
BEGIN
WHILE true LOOP
NULL;
END LOOP;
END
$$;Output yang Dihasilkan:
psql:loop-tanpa-guard.sql:10: ERROR: canceling statement due to statement timeout
CONTEXT: PL/pgSQL function inline_code_block line 3 at WHILEPostgreSQL membatalkan blok dengan canceling statement due to statement timeout — file ini sengaja diberi atribut error agar pesan pembatalannya yang nyata ikut terekam. Dua pelajaran sekaligus: server tidak mendeteksi “loop tak berujung” secara ajaib (ia hanya membatalkan berdasarkan waktu), dan statement_timeout layak dipasang di sesi maintenance Anda sebagai sabuk pengaman terakhir.
WHILE: Ulangi Selama Kondisi Terpenuhi #
WHILE memindahkan kondisi ke depan — badan berjalan nol kali pun bila kondisi sudah salah sejak awal:
-- WHILE: ulangi selama saldo belum mencapai target
CREATE FUNCTION bulan_menuju_target(target numeric, setor_per_bulan numeric)
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
v_saldo numeric := 0;
v_bulan integer := 0;
BEGIN
WHILE v_saldo < target LOOP
v_bulan := v_bulan + 1;
v_saldo := v_saldo * 1.05 + setor_per_bulan;
END LOOP;
RETURN v_bulan;
END
$$;
SELECT bulan_menuju_target(1000000, 100000) AS target_sejuta;Output yang Dihasilkan:
CREATE FUNCTION
target_sejuta
---------------
9
(1 row)Simulasi tabungan berbunga 5% dengan setoran Rp100.000 per bulan: saldo melipat hingga melewati Rp1.000.000 pada bulan kesembilan. Perhatikan urutan di dalam badan: v_bulan ditambah dulu, baru saldo dihitung — jadi bulan yang dikembalikan menghitung putaran yang menembus target.
FOR: Rentang Integer #
Perulangan paling umum — “untuk i dari a sampai b” — punya bentuk khusus yang paling ringkas:
-- FOR dengan rentang integer: maju dan mundur (REVERSE)
CREATE FUNCTION jumlah_pertama(n integer)
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
v_total integer := 0;
BEGIN
FOR i IN 1..n LOOP
v_total := v_total + i;
END LOOP;
RETURN v_total;
END
$$;
CREATE FUNCTION hitung_mundur(dari integer)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_teks text := '';
BEGIN
FOR i IN REVERSE dari..1 LOOP
v_teks := v_teks || i || ' ';
END LOOP;
RETURN rtrim(v_teks);
END
$$;
SELECT jumlah_pertama(10) AS total_1_sampai_10,
jumlah_pertama(100) AS total_1_sampai_100;
SELECT hitung_mundur(5) AS mundur;Output yang Dihasilkan:
CREATE FUNCTION
CREATE FUNCTION
total_1_sampai_10 | total_1_sampai_100
-------------------+--------------------
55 | 5050
(1 row)
mundur
-----------
5 4 3 2 1
(1 row)FOR i IN 1..n LOOP— variabelidideklarasikan otomatis sebagaiintegerkhusus loop; Anda tidak boleh (dan tidak perlu) mendeklarasikannya diDECLARE.FOR i IN REVERSE dari..1 LOOP— kataREVERSEmembalik arah:5 4 3 2 1. Rentang bawah dan atas tetap ditulis kecil-dulu meski arah mundur.- Hasil
jumlah_pertama(100)—5050— legendaris milik Gauss; loop-nya jujur menghitung satu per satu.
Rentang juga bisa melangkah: BY 2 misalnya (FOR i IN 1..9 BY 2) mengambil 1, 3, 5, 7, 9.
FOR IN SELECT dan FOREACH #
Dua bentuk FOR sisanya bekerja pada kumpulan data. FOR ... IN SELECT melalui baris hasil query, dan FOREACH melalui elemen array:
-- FOR IN SELECT: memproses hasil query baris demi baris
CREATE FUNCTION total_belanja_semua()
RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
v_total numeric := 0;
rec record;
BEGIN
FOR rec IN SELECT nama, belanja FROM pelanggan ORDER BY id LOOP
v_total := v_total + rec.belanja;
END LOOP;
RETURN v_total;
END
$$;
CREATE FUNCTION ringkasan_pelanggan()
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_teks text := '';
rec record;
BEGIN
FOR rec IN SELECT nama, belanja FROM pelanggan ORDER BY id LOOP
v_teks := v_teks || rec.nama || ': ' || round(rec.belanja) || '; ';
END LOOP;
RETURN rtrim(v_teks, '; ');
END
$$;
SELECT total_belanja_semua() AS total_semua;
SELECT ringkasan_pelanggan() AS ringkasan;Output yang Dihasilkan:
CREATE FUNCTION
CREATE FUNCTION
total_semua
-------------
1265000.00
(1 row)
ringkasan
---------------------------------------------------------------
Budi Santoso: 850000; Siti Rahayu: 320000; Ani Kartika: 95000
(1 row)FOR rec IN SELECT nama, belanja FROM pelanggan ORDER BY id LOOP— variabelrecbertiperecordotomatis menampung satu baris per putaran; anggotanya diakses denganrec.nama,rec.belanja. Function pertama menjumlahkan seluruh belanja (1265000.00), function kedua merangkai teks per pelanggan — pekerjaan yang di chapter Return Type kita delegasikan keRETURN QUERY, kini dikerjakan manual karena tiap baris diproses.FOREACHsudah kita pakai di fileloop-exit-continueuntuk arraynumeric[]— bedanya denganFOR,FOREACHmenyusuri elemen array di memori, bukan hasil query.ORDER BY iddi query sumber bukan hiasan: tanpa ia, urutan pemrosesan baris tidak terjamin.
Peta ringkas seluruh perulangan:
| Bentuk | Kapan berputar | Pintu keluar alami |
|---|---|---|
LOOP | selamanya | EXIT/EXIT WHEN (wajib!) |
WHILE kondisi LOOP | selama kondisi benar | kondisi jadi salah |
FOR i IN a..b LOOP | sekali per nilai rentang | rentang habis |
FOR rec IN SELECT ... | sekali per baris hasil | baris habis |
FOREACH x IN ARRAY ... | sekali per elemen | elemen habis |
Dengan percabangan dan perulangan di tangan, function kita sudah menjadi program sungguhan. Namun seluruh contoh di chapter ini masih memegang satu baris dalam skalar dan record anonim. Padahal record dan %ROWTYPE punya perilaku tersendiri — terutama saat ditugasi memindahkan baris antar tabel dan diuji IS NULL. Chapter berikutnya, Record dan Row Type, mengupasnya tuntas.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
case-statement.sql
Unduh berkas-- CASE statement: bentuk searched dan bentuk simple
CREATE FUNCTION kategori_pelanggan(total_belanja numeric)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_label text;
BEGIN
-- Searched CASE: kondisi bebas, dicek berurutan dari atas
CASE
WHEN total_belanja >= 500000 THEN v_label := 'prioritas';
WHEN total_belanja >= 200000 THEN v_label := 'loyal';
ELSE v_label := 'baru';
END CASE;
RETURN v_label;
END
$$;
CREATE FUNCTION label_level(level_member text)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_label text;
BEGIN
-- Simple CASE: satu nilai dibandingkan terhadap daftar kandidat
CASE level_member
WHEN 'gold' THEN v_label := 'Anggota Emas';
WHEN 'silver' THEN v_label := 'Anggota Perak';
ELSE v_label := 'Anggota Biasa';
END CASE;
RETURN v_label;
END
$$;
SELECT kategori_pelanggan(850000) AS k1,
kategori_pelanggan(320000) AS k2,
kategori_pelanggan(95000) AS k3;
SELECT label_level('gold') AS l1,
label_level('silver') AS l2,
label_level('biasa') AS l3;
for-query.sql
Unduh berkas-- FOR IN SELECT: memproses hasil query baris demi baris
CREATE FUNCTION total_belanja_semua()
RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
v_total numeric := 0;
rec record;
BEGIN
FOR rec IN SELECT nama, belanja FROM pelanggan ORDER BY id LOOP
v_total := v_total + rec.belanja;
END LOOP;
RETURN v_total;
END
$$;
CREATE FUNCTION ringkasan_pelanggan()
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_teks text := '';
rec record;
BEGIN
FOR rec IN SELECT nama, belanja FROM pelanggan ORDER BY id LOOP
v_teks := v_teks || rec.nama || ': ' || round(rec.belanja) || '; ';
END LOOP;
RETURN rtrim(v_teks, '; ');
END
$$;
SELECT total_belanja_semua() AS total_semua;
SELECT ringkasan_pelanggan() AS ringkasan;
for-range.sql
Unduh berkas-- FOR dengan rentang integer: maju dan mundur (REVERSE)
CREATE FUNCTION jumlah_pertama(n integer)
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
v_total integer := 0;
BEGIN
FOR i IN 1..n LOOP
v_total := v_total + i;
END LOOP;
RETURN v_total;
END
$$;
CREATE FUNCTION hitung_mundur(dari integer)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_teks text := '';
BEGIN
FOR i IN REVERSE dari..1 LOOP
v_teks := v_teks || i || ' ';
END LOOP;
RETURN rtrim(v_teks);
END
$$;
SELECT jumlah_pertama(10) AS total_1_sampai_10,
jumlah_pertama(100) AS total_1_sampai_100;
SELECT hitung_mundur(5) AS mundur;
if-diskon.sql
Unduh berkas-- IF / ELSIF / ELSE: diskon bertingkat berdasarkan level member
CREATE TABLE pelanggan (
id integer PRIMARY KEY,
nama text NOT NULL,
level text NOT NULL,
poin integer NOT NULL DEFAULT 0,
belanja numeric(12,2) NOT NULL DEFAULT 0
);
INSERT INTO pelanggan (id, nama, level, poin, belanja) VALUES
(1, 'Budi Santoso', 'gold', 1200, 850000.00),
(2, 'Siti Rahayu', 'silver', 450, 320000.00),
(3, 'Ani Kartika', 'biasa', 80, 95000.00);
CREATE FUNCTION diskon_member(level_member text, total_belanja numeric)
RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
v_diskon numeric;
BEGIN
IF level_member = 'gold' THEN
v_diskon := total_belanja * 0.15;
ELSIF level_member = 'silver' THEN
v_diskon := total_belanja * 0.10;
ELSE
v_diskon := total_belanja * 0.05;
END IF;
RETURN round(v_diskon, 2);
END
$$;
SELECT nama, level, belanja,
diskon_member(level, belanja) AS diskon
FROM pelanggan
ORDER BY id;
loop-exit-continue.sql
Unduh berkas-- LOOP tanpa kondisi, dikendalikan EXIT WHEN dan CONTINUE WHEN
CREATE FUNCTION hitung_cicilan(total numeric, maks_cicilan integer)
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
v_cicilan_ke integer := 0;
v_sisa numeric := total;
BEGIN
LOOP
v_cicilan_ke := v_cicilan_ke + 1;
v_sisa := v_sisa - 200000;
-- Dua pintu keluar: sisa habis, atau guard memutuskan cukup
EXIT WHEN v_sisa <= 0 OR v_cicilan_ke >= maks_cicilan;
END LOOP;
RETURN v_cicilan_ke;
END
$$;
CREATE FUNCTION hitung_positif(angka numeric[])
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
v_a numeric;
v_ada integer := 0;
BEGIN
FOREACH v_a IN ARRAY angka LOOP
CONTINUE WHEN v_a < 0;
v_ada := v_ada + 1;
END LOOP;
RETURN v_ada;
END
$$;
SELECT hitung_cicilan(350000, 12) AS ringan,
hitung_cicilan(1500000, 5) AS dihentikan_guard;
SELECT hitung_positif(ARRAY[10, -5, 20, -1, 30]::numeric[]) AS jumlah_positif;
loop-tanpa-guard.sql
Unduh berkas-- Loop tanpa pintu keluar: jaring pengaman statement_timeout yang membatalkannya
SET statement_timeout = '200ms';
DO $$
BEGIN
WHILE true LOOP
NULL;
END LOOP;
END
$$;
while-loop.sql
Unduh berkas-- WHILE: ulangi selama saldo belum mencapai target
CREATE FUNCTION bulan_menuju_target(target numeric, setor_per_bulan numeric)
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
v_saldo numeric := 0;
v_bulan integer := 0;
BEGIN
WHILE v_saldo < target LOOP
v_bulan := v_bulan + 1;
v_saldo := v_saldo * 1.05 + setor_per_bulan;
END LOOP;
RETURN v_bulan;
END
$$;
SELECT bulan_menuju_target(1000000, 100000) AS target_sejuta;