Sejauh ini semua query yang kita tulis bersifat deklaratif: kita mendeskripsikan hasil yang diinginkan — siapa pelanggan dengan belanja terbesar, berapa total per kategori — dan PostgreSQL yang memutuskan cara menghitungnya. Pendekatan ini sangat kuat, tetapi sebagian aturan bisnis tidak bisa diringkas menjadi satu ekspresi: kasir Toko Maju Jaya misalnya perlu menghitung diskon berjenjang, mencatat hasilnya ke tabel laporan, lalu menyiapkan baris berikutnya — langkah demi langkah dengan nilai antara yang saling bergantung. Untuk kebutuhan seperti itu PostgreSQL menyediakan PL/pgSQL, bahasa prosedural yang berjalan di dalam server database. Chapter ini memperkenalkan posisi PL/pgSQL di antara SQL murni, membuktikan bahwa bahasa tersebut sudah terpasang sejak awal, dan memberi Anda rasa pertama menjalankannya lewat blok anonim DO.

Kapan SQL Deklaratif Sudah Cukup #

Sebelum melompat ke bahasa prosedural, mari kita jujur menilai batas SQL. Banyak aturan “bertingkat” sebenarnya tetap bisa dihitung dengan ekspresi CASE di dalam satu query. Tabel pelanggan berikut memuat data belanja empat anggota Toko Maju Jaya:

-- Toko Maju Jaya: hitung diskon berjenjang murni dengan SQL deklaratif
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),
    (4, 'Rudi Hartono', 'gold',    950, 610000.00);

SELECT nama,
       belanja,
       round(CASE WHEN belanja >= 500000 THEN belanja * 0.15
                  WHEN belanja >= 200000 THEN belanja * 0.10
                  ELSE 0
             END, 2) AS diskon
FROM pelanggan
ORDER BY id;

Output yang Dihasilkan:

text
CREATE TABLE
INSERT 0 4
     nama     |  belanja  |  diskon   
--------------+-----------+-----------
 Budi Santoso | 850000.00 | 127500.00
 Siti Rahayu  | 320000.00 |  32000.00
 Ani Kartika  |  95000.00 |      0.00
 Rudi Hartono | 610000.00 |  91500.00
(4 rows)

Query tersebut menghitung diskon berjenjang — 15% untuk belanja di atas Rp500.000, 10% untuk di atas Rp200.000, 0% untuk sisanya — tanpa satu baris kode prosedural pun. Selama logika bisa dinyatakan sebagai ekspresi atas satu baris, CASE, fungsi bawaan, dan subquery biasanya sudah memadai, dan versi deklaratif hampir selalu lebih pendek serta lebih mudah dioptimasi oleh planner.

Masalah muncul ketika logika membutuhkan keadaan antara (nilai yang dihitung lalu dipakai pada langkah berikutnya), perulangan (memproses baris demi baris dengan keputusan berbeda-beda), atau penanganan error (melanjutkan pekerjaan ketika satu langkah gagal). Tiga hal itu bukan wilayah ekspresi SQL; di situlah PL/pgSQL mengambil alih.

PL/pgSQL: Bahasa Prosedural yang Sudah Terpasang #

PL/pgSQL (Procedural Language/PostgreSQL Structured Query Language) adalah bahasa prosedural buatan komunitas PostgreSQL: berisi variabel, percabangan, perulangan, dan eksekusi SQL sebagai bagian dari program. Berbeda dari banyak database lain, PostgreSQL tidak membuat Anda mengaktifkannya — initdb memasang PL/pgSQL ke database template1, sehingga setiap CREATE DATABASE baru mewarisinya secara otomatis:

-- PL/pgSQL sudah terdaftar sebagai language di setiap database baru
SELECT lanname, lanispl, lanpltrusted
FROM pg_language
ORDER BY lanname;

-- Dan terpasang sebagai extension bawaan lewat template1
SELECT extname
FROM pg_extension
ORDER BY extname;

Output yang Dihasilkan:

text
 lanname  | lanispl | lanpltrusted 
----------+---------+--------------
 c        | f       | f
 internal | f       | f
 plpgsql  | t       | t
 sql      | f       | t
(4 rows)

 extname 
---------
 plpgsql
(1 row)

Dua output tersebut adalah bukti hidupnya: baris plpgsql di pg_language menandai bahasa yang terdaftar dan trusted (aman dijalankan pengguna biasa), sedangkan baris plpgsql di pg_extension menunjukkan paketnya ikut terpasang dari template. Anda tidak perlu menjalankan CREATE EXTENSION plpgsql pada PostgreSQL modern — perintah itu hanya relevan pada database warisan yang dibuat sebelum praktik otomatis ini ada.

PL/pgSQL juga bukan satu-satunya bahasa yang bisa dipakai. PostgreSQL dirancang sebagai loadable language host:

BahasaStatusKarakter
sqlterpasangfunction berisi query murni, tanpa logika prosedural
plpgsqlterpasang, trustedprosedural penuh, standar de facto
plpython3uvia extension, untrustedekspresi Python, hanya untuk superuser
plperl / plperluvia extensionPerl, varian u (untrusted) tanpa pembatasan

Sepanjang section ini kita memakai plpgsql karena bahasa tersebut terpasang di mana saja, terdokumentasi paling lengkap, dan menjadi bahasa yang dipakai trigger. Konsep-konsepnya — variabel, parameter, percabangan — juga berpindah mulus ke bahasa prosedural lain.

Blok Anonim DO: PL/pgSQL Tanpa Function #

Cara tercepat mencoba PL/pgSQL tanpa menyimpan apa pun adalah statement DO: sebuah blok kode yang dikompilasi, dijalankan sekali, lalu dibuang. Bentuknya adalah string konstan — biasanya dengan dollar quoting $$...$$ supaya tanda kutip di dalam kode tidak perlu di-escape:

sql do-pertama.sql
-- Blok anonim DO: logika PL/pgSQL sekali jalan, tanpa CREATE FUNCTION
CREATE TABLE laporan_diskon (
    total_belanja numeric,
    diskon        numeric,
    total_bayar   numeric
);

DO $$
DECLARE
    v_total  numeric := 250000;
    v_diskon numeric;
    v_bayar  numeric;
BEGIN
    RAISE NOTICE 'Menghitung diskon untuk total belanja %', v_total;

    v_diskon := v_total * 0.10;
    v_bayar  := v_total - v_diskon;

    INSERT INTO laporan_diskon
    VALUES (v_total, round(v_diskon, 2), round(v_bayar, 2));
END
$$;

SELECT * FROM laporan_diskon;

Output yang Dihasilkan:

text
CREATE TABLE
DO
 total_belanja |  diskon  | total_bayar 
---------------+----------+-------------
        250000 | 25000.00 |   225000.00
(1 row)

Mari kita bedah blok di atas baris per baris:

  1. DECLARE: bagian deklarasi variabel — v_total langsung diisi nilai awal 250000, sedangkan v_diskon dan v_bayar baru akan diisi pada bagian BEGIN.
  2. v_diskon := v_total * 0.10;: operator := mengisi variabel dengan hasil ekspresi — perilaku assignment khas bahasa prosedural, bukan perbandingan.
  3. RAISE NOTICE ...: mencetak pesan diagnostik. psql menampilkannya sebagai baris NOTICE: di terminal (aliran stderr), sehingga pada output terekam di atas hanya tampak tag DO dari statement dan hasil SELECT tabel — pesan NOTICE-nya muncul di layar Anda saat menjalankan file ini sendiri.
  4. INSERT INTO laporan_diskon ...: blok DO boleh menjalankan SQL apa pun, termasuk menulis ke tabel — di sinilah nilai antara v_diskon dan v_bayar diambil dan dibuktikan lewat SELECT setelahnya.

Anatomi Blok: DECLARE, BEGIN, EXCEPTION, END #

Semua kode PL/pgSQL — baik di dalam DO maupun function — hidup dalam blok dengan kerangka yang sama. Contoh berikut memperlihatkan keempat bagiannya sekaligus:

-- Anatomi blok PL/pgSQL: DECLARE, BEGIN, EXCEPTION, END
CREATE TABLE hasil_hitung (
    langkah text,
    nilai   text
);

DO $$
DECLARE
    v_total  numeric := 250000;
    v_diskon numeric;
BEGIN
    v_diskon := v_total * 0.10;
    INSERT INTO hasil_hitung VALUES ('diskon', round(v_diskon, 2)::text);

    BEGIN
        INSERT INTO hasil_hitung VALUES ('pembagian', (100 / 0)::text);
    EXCEPTION WHEN division_by_zero THEN
        INSERT INTO hasil_hitung VALUES ('pembagian', 'gagal: pembagi nol');
    END;
END
$$;

SELECT langkah, nilai FROM hasil_hitung ORDER BY langkah;

Output yang Dihasilkan:

text
CREATE TABLE
DO
  langkah  |       nilai        
-----------+--------------------
 diskon    | 25000.00
 pembagian | gagal: pembagi nol
(2 rows)

Kerangka blok terdiri atas:

  1. DECLARE ... (opsional): daftar variabel beserta tipe dan nilai awalnya.
  2. BEGIN ... END: badan eksekusi — statement SQL dan perintah prosedural dijalankan berurutan dari atas ke bawah.
  3. EXCEPTION WHEN ... THEN (opsional): penangkap error. Bagian ini harus berada setelah statement terakhir BEGIN dan sebelum END; tanpa klausa ini, error apa pun langsung membatalkan seluruh blok.
  4. Blok bersarang: BEGIN ... EXCEPTION ... END di dalam blok luar membentuk unit penangkap error tersendiri — di atas, pembagian 100 / 0 ditangkap WHEN division_by_zero tanpa membatalkan langkah sebelumnya. Penanganan error lebih dalam menjadi bahasan chapter Exception Handling.

Dengan kerangka blok di tangan, batas DO juga jelas terlihat: kode tersebut hilang begitu selesai — tidak bisa dipanggil ulang, tidak punya nama, tidak menerima parameter. Langkah alami berikutnya adalah membungkus blok yang sama menjadi function bernama yang bisa dipanggil dari query mana saja: SELECT hitung_diskon(250000). Chapter berikutnya, Membuat Function, memulai dari sana.

Referensi #

Berkas Sumber

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

bahasa-terpasang.sql

Unduh berkas
sql
-- PL/pgSQL sudah terdaftar sebagai language di setiap database baru
SELECT lanname, lanispl, lanpltrusted
FROM pg_language
ORDER BY lanname;

-- Dan terpasang sebagai extension bawaan lewat template1
SELECT extname
FROM pg_extension
ORDER BY extname;

do-pertama.sql

Unduh berkas
sql
-- Blok anonim DO: logika PL/pgSQL sekali jalan, tanpa CREATE FUNCTION
CREATE TABLE laporan_diskon (
    total_belanja numeric,
    diskon        numeric,
    total_bayar   numeric
);

DO $$
DECLARE
    v_total  numeric := 250000;
    v_diskon numeric;
    v_bayar  numeric;
BEGIN
    RAISE NOTICE 'Menghitung diskon untuk total belanja %', v_total;

    v_diskon := v_total * 0.10;
    v_bayar  := v_total - v_diskon;

    INSERT INTO laporan_diskon
    VALUES (v_total, round(v_diskon, 2), round(v_bayar, 2));
END
$$;

SELECT * FROM laporan_diskon;

sql-deklaratif.sql

Unduh berkas
sql
-- Toko Maju Jaya: hitung diskon berjenjang murni dengan SQL deklaratif
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),
    (4, 'Rudi Hartono', 'gold',    950, 610000.00);

SELECT nama,
       belanja,
       round(CASE WHEN belanja >= 500000 THEN belanja * 0.15
                  WHEN belanja >= 200000 THEN belanja * 0.10
                  ELSE 0
             END, 2) AS diskon
FROM pelanggan
ORDER BY id;

struktur-blok.sql

Unduh berkas
sql
-- Anatomi blok PL/pgSQL: DECLARE, BEGIN, EXCEPTION, END
CREATE TABLE hasil_hitung (
    langkah text,
    nilai   text
);

DO $$
DECLARE
    v_total  numeric := 250000;
    v_diskon numeric;
BEGIN
    v_diskon := v_total * 0.10;
    INSERT INTO hasil_hitung VALUES ('diskon', round(v_diskon, 2)::text);

    BEGIN
        INSERT INTO hasil_hitung VALUES ('pembagian', (100 / 0)::text);
    EXCEPTION WHEN division_by_zero THEN
        INSERT INTO hasil_hitung VALUES ('pembagian', 'gagal: pembagi nol');
    END;
END
$$;

SELECT langkah, nilai FROM hasil_hitung ORDER BY langkah;