Ekspresi CASE untuk diskon berjenjang dari chapter sebelumnya bekerja, tetapi setiap aplikasi yang membutuhkan perhitungan itu harus menulis ulang ekspresinya — dan ketika kebijakan diskon berubah, Anda harus memburu seluruh tempat tersebut satu per satu. Function menyelesaikan masalah duplikasi ini: logika diberi nama, disimpan di dalam database, dan dipanggil seperti fungsi biasa dari SELECT mana pun. Chapter ini menulis function pertama dalam dua rasa — murni SQL dan PL/pgSQL — lalu melatih rutinitas operasionalnya: melihat daftar function, menghapus, memperbarui, dan menandai kategori volatilitas yang menentukan di mana function boleh dipakai.
Function Pertama: LANGUAGE sql #
Function paling sederhana tidak butuh PL/pgSQL sama sekali. Dengan LANGUAGE sql, isi function adalah query biasa — dan karena query boleh berupa ekspresi SELECT, satu baris pun sudah menjadi function yang sah:
-- Function paling sederhana: bahasa SQL, isinya satu query
CREATE FUNCTION format_rupiah(nilai numeric) RETURNS text
LANGUAGE sql
AS $$
SELECT 'Rp' || replace(to_char(nilai, 'FM999G999G999'), ',', '.')
$$;
SELECT format_rupiah(85000) AS kopi,
format_rupiah(1250000) AS beras,
format_rupiah(500) AS permen;Output yang Dihasilkan:
CREATE FUNCTION
kopi | beras | permen
----------+-------------+--------
Rp85.000 | Rp1.250.000 | Rp500
(1 row)Anatomi statement di atas:
CREATE FUNCTION format_rupiah(nilai numeric) RETURNS text: nama function, satu parameternilaibertipenumeric, dan hasil keluaran bertipetext. Parameter ini otomatis berperan sebagai variabel bernama di dalam body.LANGUAGE sql: menandakan body berisi SQL murni — tanpaDECLARE, tanpaBEGIN/END.AS $$ ... $$: body dibungkus dollar quoting supaya tanda kutip di dalamnya aman. Query terakhir inilah yang menjadi nilai kembalian: hasil kolom pertama baris pertama.
Pemanggilannya persis seperti fungsi bawaan PostgreSQL — format_rupiah(85000) menghasilkan Rp85.000. Untuk logika satu ekspresi, LANGUAGE sql adalah pilihan paling ringkas: tanpa kompilasi PL/pgSQL, dan planner bahkan bisa menyisipkan (meng-inline) body-nya ke query pemanggil sehingga performanya nyaris sama dengan menulis ekspresi secara langsung.
Function PL/pgSQL: hitung_diskon #
Ketika perhitungan butuh variabel antara atau lebih dari satu langkah, ganti bahasanya ke plpgsql dan bungkus blok yang kita kenal dari chapter Pengenalan PL/pgSQL:
-- Versi PL/pgSQL: butuh variabel antara sebelum mengembalikan hasil
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);
CREATE FUNCTION hitung_diskon(total_belanja numeric) RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
v_diskon numeric;
BEGIN
v_diskon := CASE WHEN total_belanja >= 500000 THEN total_belanja * 0.15
WHEN total_belanja >= 200000 THEN total_belanja * 0.10
ELSE 0
END;
RETURN round(v_diskon, 2);
END
$$;
SELECT nama, belanja, hitung_diskon(belanja) AS diskon
FROM pelanggan
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 4
CREATE FUNCTION
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)Bedah baris-baris pentingnya:
CREATE TABLE pelanggan ...: data contoh Toko Maju Jaya — setiap chapter section ini membangun ulang tabelnya sendiri supaya contoh tetap self-contained.DECLARE v_diskon numeric;: variabel lokal penampung nilai antara; parametertotal_belanjasudah otomatis tersedia sebagai variabel bernama di dalam body.v_diskon := CASE ... END;: assignment:=mengisi variabel dengan hasil ekspresiCASE— logika berjenjang yang sama dengan chapter sebelumnya, kini hidup di satu tempat bernama.RETURN round(v_diskon, 2);: mengakhiri function dan menyerahkan hasil ke pemanggil.SELECT ... hitung_diskon(belanja) ...: function dipanggil per baris di daftar kolomSELECT— perlakuan yang sama dengan fungsi bawaan sepertiround()atauupper().
Melihat dan Menghapus Function: \df dan DROP FUNCTION #
Function tersimpan sebagai objek database, jadi ada meta-command untuk mengawasinya. \df milik psql menampilkan daftar function milik pengguna beserta tipe argumennya — informasi yang disebut signature:
-- \df: daftar function milik pengguna di database saat ini
\df
-- DROP menyebut signature lengkap: nama + tipe argumen
DROP FUNCTION format_rupiah(numeric);
-- format_rupiah hilang dari daftar, hitung_diskon tetap ada
\dfOutput yang Dihasilkan:
List of functions
Schema | Name | Result data type | Argument data types | Type
--------+---------------+------------------+-----------------------+------
public | format_rupiah | text | nilai numeric | func
public | hitung_diskon | numeric | total_belanja numeric | func
(2 rows)
DROP FUNCTION
List of functions
Schema | Name | Result data type | Argument data types | Type
--------+---------------+------------------+-----------------------+------
public | hitung_diskon | numeric | total_belanja numeric | func
(1 row)Kolom Argument data types itulah kunci penghapusan: DROP FUNCTION menuntut nama dan tipe argumen. Percobaan menghapus hitung_diskon dengan tipe yang salah gagal keras:
-- DROP dengan tipe argumen yang salah: function tidak ditemukan
DROP FUNCTION hitung_diskon(integer);Output yang Dihasilkan:
psql:drop-signature-salah.sql:2: ERROR: function hitung_diskon(integer) does not existPostgreSQL menjawab function hitung_diskon(integer) does not exist karena satu-satunya hitung_diskon yang terdaftar menerima numeric. Ketelitian ini bukan cerewet: dua function boleh berbagi nama dengan daftar argumen berbeda (overloading), jadi tipe argumen adalah satu-satunya pembeda yang pasti. DROP FUNCTION format_rupiah(numeric) di file sebelumnya berhasil karena signature-nya tepat.
CREATE OR REPLACE: Memperbarui Isi Function #
Kebijakan diskon Toko Maju Jaya berubah — diskon 20% kini mulai belanja Rp750.000. Alih-alih DROP lalu CREATE (yang sempat menjadikan function hilang sejenak), CREATE OR REPLACE menukar body secara atomik:
-- CREATE OR REPLACE: ganti isi function tanpa menjatuhkannya
CREATE OR REPLACE FUNCTION hitung_diskon(total_belanja numeric) RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
v_diskon numeric;
BEGIN
-- Kebijakan baru Toko Maju Jaya: diskon 20% mulai belanja Rp750.000
v_diskon := CASE WHEN total_belanja >= 750000 THEN total_belanja * 0.20
WHEN total_belanja >= 200000 THEN total_belanja * 0.10
ELSE 0
END;
RETURN round(v_diskon, 2);
END
$$;
SELECT nama, belanja, hitung_diskon(belanja) AS diskon
FROM pelanggan
ORDER BY id;Output yang Dihasilkan:
CREATE FUNCTION
nama | belanja | diskon
--------------+-----------+-----------
Budi Santoso | 850000.00 | 170000.00
Siti Rahayu | 320000.00 | 32000.00
Ani Kartika | 95000.00 | 0.00
Rudi Hartono | 610000.00 | 61000.00
(4 rows)Diskon Budi melonjak dari 127500.00 ke 170000.00 dan Rudi dari 91500.00 ke 61000.00, sementara nama function dan cara memanggilnya tidak berubah sedikit pun — pemanggil tidak perlu tahu kebijakan di dalamnya berganti.
Kategori Volatilitas: IMMUTABLE, STABLE, VOLATILE #
Satu klausa kecil menentukan seberapa jauh planner boleh mempercayai function Anda:
| Kategori | Jaminan | Contoh | Boleh di ekspresi index? |
|---|---|---|---|
IMMUTABLE | argumen sama → hasil sama, selamanya | format_rupiah, harga_setelah_pajak | ya |
STABLE | argumen sama → hasil sama dalam satu statement | now(), function yang membaca tabel konfigurasi | ya, untuk kondisi tertentu |
VOLATILE | tidak ada jaminan; bisa berbeda tiap pemanggilan | random(), kode_promo, function yang menulis tabel | tidak |
Kategori default adalah VOLATILE — pilihan paling konservatif. Konsekuensi praktisnya terlihat pada index: karena nilai index harus tetap benar selama baris tidak berubah, hanya function IMMUTABLE yang diterima sebagai ekspresi index:
-- IMMUTABLE: hasil hanya bergantung pada argumen, selamanya
CREATE FUNCTION harga_setelah_pajak(harga numeric) RETURNS numeric
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
RETURN round(harga * 1.11, 2);
END
$$;
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(12,2) NOT NULL,
stok integer NOT NULL DEFAULT 0
);
INSERT INTO produk (id, nama, harga, stok) VALUES
(1, 'Kopi Gayo 250g', 85000.00, 40),
(2, 'Teh Melati 100g', 15000.00, 120),
(3, 'Gula Kristal 1kg', 16000.00, 75),
(4, 'Beras Premium 5kg', 68000.00, 25);
-- VOLATILE: hasil bisa berbeda pada setiap pemanggilan
CREATE FUNCTION kode_promo() RETURNS text
LANGUAGE plpgsql
VOLATILE
AS $$
DECLARE
v_kode text;
BEGIN
v_kode := 'PROMO-' || to_char(floor(random() * 9000 + 1000), 'FM9999');
RETURN v_kode;
END
$$;
-- Function IMMUTABLE boleh menjadi ekspresi index
CREATE INDEX idx_harga_pajak ON produk (harga_setelah_pajak(harga));
\d produkOutput yang Dihasilkan:
CREATE FUNCTION
CREATE TABLE
INSERT 0 4
CREATE FUNCTION
CREATE INDEX
Table "public.produk"
Column | Type | Collation | Nullable | Default
--------+---------------+-----------+----------+---------
id | integer | | not null |
nama | text | | not null |
harga | numeric(12,2) | | not null |
stok | integer | | not null | 0
Indexes:
"produk_pkey" PRIMARY KEY, btree (id)
"idx_harga_pajak" btree (harga_setelah_pajak(harga))Index idx_harga_pajak tercatat pada daftar Indexes di output \d produk dengan ekspresi harga_setelah_pajak(harga) — harga pajak PPN 11% setiap produk kini terindeks tanpa perlu kolom tambahan. Sebaliknya, function kode_promo() yang menghasilkan kode acak ditandai VOLATILE; memaksa function seperti itu masuk index ditolak:
-- Function VOLATILE ditolak saat dipakai sebagai ekspresi index
CREATE INDEX idx_promo ON produk (kode_promo());Output yang Dihasilkan:
psql:index-volatile.sql:2: ERROR: functions in index expression must be marked IMMUTABLEFunction pertama kita hanya menerima satu parameter masuk dan mengembalikan satu nilai. Padahal data mengalir dua arah: function bisa mengembalikan beberapa nilai lewat parameter keluar, menerima daftar panjang variabel, dan memanggil dengan argumen bernama. Chapter berikutnya, Parameter Function, membuka pintu-pintu itu.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
df-dan-drop.sql
Unduh berkas-- \df: daftar function milik pengguna di database saat ini
\df
-- DROP menyebut signature lengkap: nama + tipe argumen
DROP FUNCTION format_rupiah(numeric);
-- format_rupiah hilang dari daftar, hitung_diskon tetap ada
\df
drop-signature-salah.sql
Unduh berkas-- DROP dengan tipe argumen yang salah: function tidak ditemukan
DROP FUNCTION hitung_diskon(integer);
function-plpgsql.sql
Unduh berkas-- Versi PL/pgSQL: butuh variabel antara sebelum mengembalikan hasil
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);
CREATE FUNCTION hitung_diskon(total_belanja numeric) RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
v_diskon numeric;
BEGIN
v_diskon := CASE WHEN total_belanja >= 500000 THEN total_belanja * 0.15
WHEN total_belanja >= 200000 THEN total_belanja * 0.10
ELSE 0
END;
RETURN round(v_diskon, 2);
END
$$;
SELECT nama, belanja, hitung_diskon(belanja) AS diskon
FROM pelanggan
ORDER BY id;
function-sql.sql
Unduh berkas-- Function paling sederhana: bahasa SQL, isinya satu query
CREATE FUNCTION format_rupiah(nilai numeric) RETURNS text
LANGUAGE sql
AS $$
SELECT 'Rp' || replace(to_char(nilai, 'FM999G999G999'), ',', '.')
$$;
SELECT format_rupiah(85000) AS kopi,
format_rupiah(1250000) AS beras,
format_rupiah(500) AS permen;
index-volatile.sql
Unduh berkas-- Function VOLATILE ditolak saat dipakai sebagai ekspresi index
CREATE INDEX idx_promo ON produk (kode_promo());
or-replace.sql
Unduh berkas-- CREATE OR REPLACE: ganti isi function tanpa menjatuhkannya
CREATE OR REPLACE FUNCTION hitung_diskon(total_belanja numeric) RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
v_diskon numeric;
BEGIN
-- Kebijakan baru Toko Maju Jaya: diskon 20% mulai belanja Rp750.000
v_diskon := CASE WHEN total_belanja >= 750000 THEN total_belanja * 0.20
WHEN total_belanja >= 200000 THEN total_belanja * 0.10
ELSE 0
END;
RETURN round(v_diskon, 2);
END
$$;
SELECT nama, belanja, hitung_diskon(belanja) AS diskon
FROM pelanggan
ORDER BY id;
volatilitas.sql
Unduh berkas-- IMMUTABLE: hasil hanya bergantung pada argumen, selamanya
CREATE FUNCTION harga_setelah_pajak(harga numeric) RETURNS numeric
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
RETURN round(harga * 1.11, 2);
END
$$;
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(12,2) NOT NULL,
stok integer NOT NULL DEFAULT 0
);
INSERT INTO produk (id, nama, harga, stok) VALUES
(1, 'Kopi Gayo 250g', 85000.00, 40),
(2, 'Teh Melati 100g', 15000.00, 120),
(3, 'Gula Kristal 1kg', 16000.00, 75),
(4, 'Beras Premium 5kg', 68000.00, 25);
-- VOLATILE: hasil bisa berbeda pada setiap pemanggilan
CREATE FUNCTION kode_promo() RETURNS text
LANGUAGE plpgsql
VOLATILE
AS $$
DECLARE
v_kode text;
BEGIN
v_kode := 'PROMO-' || to_char(floor(random() * 9000 + 1000), 'FM9999');
RETURN v_kode;
END
$$;
-- Function IMMUTABLE boleh menjadi ekspresi index
CREATE INDEX idx_harga_pajak ON produk (harga_setelah_pajak(harga));
\d produk