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:

sql if-diskon.sql
-- 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:

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

  1. IF level_member = 'gold' THEN ... ELSIF ... ELSE ... END IF; — kondisi dicek berurutan dari atas; begitu satu cabang cocok, sisanya dilewati. Tanpa ELSE dan tanpa kondisi yang cocok, eksekusi langsung melompat ke END IF.
  2. Parameter level_member sengaja tidak bernama level supaya tidak bertabrakan dengan kolom level tabel pelanggan — tabrakan nama parameter dengan kolom menghasilkan error ambigu yang sudah kita temui di chapter Return Type.
  3. Pemanggilan diskon_member(level, belanja) mengirim kolom sebagai argumen; keputusan di dalam function bekerja per baris: Budi 127500.00, Siti 32000.00, Ani 4750.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:

text
CREATE FUNCTION
CREATE FUNCTION
    k1     |  k2   |  k3  
-----------+-------+------
 prioritas | loyal | baru
(1 row)

      l1      |      l2       |      l3       
--------------+---------------+---------------
 Anggota Emas | Anggota Perak | Anggota Biasa
(1 row)
  1. Searched CASE (CASE WHEN kondisi THEN ...): kondisinya ekspresi boolean bebas, dievaluasi berurutan — pasangan alami untuk rentang nilai seperti total_belanja >= 500000.
  2. Simple CASE (CASE nilai WHEN kandidat THEN ...): satu nilai dibandingkan terhadap daftar kandidat — pas untuk pemetaan kode ke label seperti gold → 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:

text
CREATE FUNCTION
CREATE FUNCTION
 ringan | dihentikan_guard 
--------+------------------
      2 |                5
(1 row)

 jumlah_positif 
----------------
              3
(1 row)
  1. LOOP ... EXIT WHEN v_sisa <= 0 OR v_cicilan_ke >= maks_cicilan; END LOOP; — function hitung_cicilan memotong Rp200.000 tiap putaran. Kondisi EXIT WHEN memuat dua syarat: sisa habis secara alami, atau counter mencapai batas maks_cicilan.
  2. Pemanggilan kedua membuktikan guard bekerja: total Rp1.500.000 sebenarnya butuh 8 putaran, tetapi maks_cicilan = 5 menghentikan loop di putaran kelima — mengembalikan 5, bukan 8. Inilah pola counter guard: batas atas yang pasti dicapai, apa pun kondisi data.
  3. CONTINUE WHEN v_a < 0; — melempar putaran berisi nilai negatif ke putaran berikutnya tanpa mengeksekusi sisa badan loop; dari lima elemen array, tiga dihitung.
  4. Bentuk polos EXIT; dan CONTINUE; (tanpa WHEN) juga sah — biasanya dipakai di dalam IF bersarang. EXIT/CONTINUE dengan 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:

text
psql:loop-tanpa-guard.sql:10: ERROR:  canceling statement due to statement timeout
CONTEXT:  PL/pgSQL function inline_code_block line 3 at WHILE

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

sql while-loop.sql
-- 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:

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

sql for-range.sql
-- 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:

text
CREATE FUNCTION
CREATE FUNCTION
 total_1_sampai_10 | total_1_sampai_100 
-------------------+--------------------
                55 |               5050
(1 row)

  mundur   
-----------
 5 4 3 2 1
(1 row)
  1. FOR i IN 1..n LOOP — variabel i dideklarasikan otomatis sebagai integer khusus loop; Anda tidak boleh (dan tidak perlu) mendeklarasikannya di DECLARE.
  2. FOR i IN REVERSE dari..1 LOOP — kata REVERSE membalik arah: 5 4 3 2 1. Rentang bawah dan atas tetap ditulis kecil-dulu meski arah mundur.
  3. 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:

sql for-query.sql
-- 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:

text
CREATE FUNCTION
CREATE FUNCTION
 total_semua 
-------------
  1265000.00
(1 row)

                           ringkasan                           
---------------------------------------------------------------
 Budi Santoso: 850000; Siti Rahayu: 320000; Ani Kartika: 95000
(1 row)
  1. FOR rec IN SELECT nama, belanja FROM pelanggan ORDER BY id LOOP — variabel rec bertipe record otomatis menampung satu baris per putaran; anggotanya diakses dengan rec.nama, rec.belanja. Function pertama menjumlahkan seluruh belanja (1265000.00), function kedua merangkai teks per pelanggan — pekerjaan yang di chapter Return Type kita delegasikan ke RETURN QUERY, kini dikerjakan manual karena tiap baris diproses.
  2. FOREACH sudah kita pakai di file loop-exit-continue untuk array numeric[] — bedanya dengan FOR, FOREACH menyusuri elemen array di memori, bukan hasil query.
  3. ORDER BY id di query sumber bukan hiasan: tanpa ia, urutan pemrosesan baris tidak terjamin.

Peta ringkas seluruh perulangan:

BentukKapan berputarPintu keluar alami
LOOPselamanyaEXIT/EXIT WHEN (wajib!)
WHILE kondisi LOOPselama kondisi benarkondisi jadi salah
FOR i IN a..b LOOPsekali per nilai rentangrentang habis
FOR rec IN SELECT ...sekali per baris hasilbaris habis
FOREACH x IN ARRAY ...sekali per elemenelemen 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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;