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:
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:
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:
| Bahasa | Status | Karakter |
|---|---|---|
sql | terpasang | function berisi query murni, tanpa logika prosedural |
plpgsql | terpasang, trusted | prosedural penuh, standar de facto |
plpython3u | via extension, untrusted | ekspresi Python, hanya untuk superuser |
plperl / plperlu | via extension | Perl, 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:
-- 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:
CREATE TABLE
DO
total_belanja | diskon | total_bayar
---------------+----------+-------------
250000 | 25000.00 | 225000.00
(1 row)Mari kita bedah blok di atas baris per baris:
DECLARE: bagian deklarasi variabel —v_totallangsung diisi nilai awal250000, sedangkanv_diskondanv_bayarbaru akan diisi pada bagianBEGIN.v_diskon := v_total * 0.10;: operator:=mengisi variabel dengan hasil ekspresi — perilaku assignment khas bahasa prosedural, bukan perbandingan.RAISE NOTICE ...: mencetak pesan diagnostik. psql menampilkannya sebagai barisNOTICE:di terminal (aliran stderr), sehingga pada output terekam di atas hanya tampak tagDOdari statement dan hasilSELECTtabel — pesan NOTICE-nya muncul di layar Anda saat menjalankan file ini sendiri.INSERT INTO laporan_diskon ...: blokDOboleh menjalankan SQL apa pun, termasuk menulis ke tabel — di sinilah nilai antarav_diskondanv_bayardiambil dan dibuktikan lewatSELECTsetelahnya.
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:
CREATE TABLE
DO
langkah | nilai
-----------+--------------------
diskon | 25000.00
pembagian | gagal: pembagi nol
(2 rows)Kerangka blok terdiri atas:
DECLARE ...(opsional): daftar variabel beserta tipe dan nilai awalnya.BEGIN ... END: badan eksekusi — statement SQL dan perintah prosedural dijalankan berurutan dari atas ke bawah.EXCEPTION WHEN ... THEN(opsional): penangkap error. Bagian ini harus berada setelah statement terakhirBEGINdan sebelumEND; tanpa klausa ini, error apa pun langsung membatalkan seluruh blok.- Blok bersarang:
BEGIN ... EXCEPTION ... ENDdi dalam blok luar membentuk unit penangkap error tersendiri — di atas, pembagian100 / 0ditangkapWHEN division_by_zerotanpa 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-- 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-- 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-- 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-- 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;