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:

text
CREATE FUNCTION
   kopi   |    beras    | permen 
----------+-------------+--------
 Rp85.000 | Rp1.250.000 | Rp500
(1 row)

Anatomi statement di atas:

  1. CREATE FUNCTION format_rupiah(nilai numeric) RETURNS text: nama function, satu parameter nilai bertipe numeric, dan hasil keluaran bertipe text. Parameter ini otomatis berperan sebagai variabel bernama di dalam body.
  2. LANGUAGE sql: menandakan body berisi SQL murni — tanpa DECLARE, tanpa BEGIN/END.
  3. 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:

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

  1. CREATE TABLE pelanggan ...: data contoh Toko Maju Jaya — setiap chapter section ini membangun ulang tabelnya sendiri supaya contoh tetap self-contained.
  2. DECLARE v_diskon numeric;: variabel lokal penampung nilai antara; parameter total_belanja sudah otomatis tersedia sebagai variabel bernama di dalam body.
  3. v_diskon := CASE ... END;: assignment := mengisi variabel dengan hasil ekspresi CASE — logika berjenjang yang sama dengan chapter sebelumnya, kini hidup di satu tempat bernama.
  4. RETURN round(v_diskon, 2);: mengakhiri function dan menyerahkan hasil ke pemanggil.
  5. SELECT ... hitung_diskon(belanja) ...: function dipanggil per baris di daftar kolom SELECT — perlakuan yang sama dengan fungsi bawaan seperti round() atau upper().

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:

sql df-dan-drop.sql
-- \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

Output yang Dihasilkan:

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

text
psql:drop-signature-salah.sql:2: ERROR:  function hitung_diskon(integer) does not exist

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

sql or-replace.sql
-- 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:

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

KategoriJaminanContohBoleh di ekspresi index?
IMMUTABLEargumen sama → hasil sama, selamanyaformat_rupiah, harga_setelah_pajakya
STABLEargumen sama → hasil sama dalam satu statementnow(), function yang membaca tabel konfigurasiya, untuk kondisi tertentu
VOLATILEtidak ada jaminan; bisa berbeda tiap pemanggilanrandom(), kode_promo, function yang menulis tabeltidak

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:

sql volatilitas.sql
-- 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

Output yang Dihasilkan:

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

text
psql:index-volatile.sql:2: ERROR:  functions in index expression must be marked IMMUTABLE

Function 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
sql
-- \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
sql
-- DROP dengan tipe argumen yang salah: function tidak ditemukan
DROP FUNCTION hitung_diskon(integer);

function-plpgsql.sql

Unduh berkas
sql
-- 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
sql
-- 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
sql
-- Function VOLATILE ditolak saat dipakai sebagai ekspresi index
CREATE INDEX idx_promo ON produk (kode_promo());

or-replace.sql

Unduh berkas
sql
-- 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
sql
-- 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