Variabel adalah bahan bakar semua logika prosedural: diskon dihitung bertahap, harga disimpan sementara, hasil antara ditimbang sebelum diputuskan. Di PL/pgSQL, variabel hidup di bagian DECLARE, diisi dengan operator :=, dan paling sering ditempa oleh SELECT INTO — perintah yang menyalin hasil query ke variabel. Chapter ini membedah keempat hal itu dari fondasinya, lalu menambah dua teknik yang membedakan function yang rapuh dari yang tahan lama: jangkar %TYPE/%ROWTYPE yang menautkan tipe variabel ke kolom tabel, serta pemeriksaan NOT FOUND yang menjaga function tetap jujur ketika query tidak menemukan apa yang dicari. Sepanjang jalan kita juga berkenalan dengan dua perilaku NULL dan ruang lingkup blok bersarang yang kerap mengejutkan pemula.

DECLARE dan Operator := #

Setiap variabel dideklarasikan sekali di bagian DECLARE dengan format nama tipe, dan boleh langsung diberi nilai awal — lewat := maupun kata kunci DEFAULT, keduanya maknanya sama:

-- Deklarasi variabel: nilai awal dengan := dan DEFAULT
CREATE TABLE hasil_hitung (
    langkah text,
    nilai   numeric
);

DO $$
DECLARE
    v_harga       numeric := 85000;   -- nilai awal lewat :=
    v_jumlah      integer DEFAULT 3;  -- DEFAULT: makna yang sama
    v_total       numeric;            -- tanpa nilai awal -> NULL
    v_total_bayar numeric;
BEGIN
    v_total := v_harga * v_jumlah;        -- assignment idiomatik
    v_total_bayar = v_total * 0.9;        -- '=' juga diterima

    INSERT INTO hasil_hitung VALUES ('total', v_total);
    INSERT INTO hasil_hitung VALUES ('total_bayar', v_total_bayar);
END
$$;

SELECT * FROM hasil_hitung ORDER BY langkah;

Output yang Dihasilkan:

text
CREATE TABLE
DO
   langkah   |  nilai   
-------------+----------
 total       |   255000
 total_bayar | 229500.0
(2 rows)

Bedah singkat polanya:

  1. v_harga numeric := 85000; — deklarasi sekaligus pengisian: satu baris, tidak ada nilai NULL yang mengendap.
  2. v_jumlah integer DEFAULT 3; — DEFAULT identik dengan := di posisi ini; pilih satu gaya dan konsistenlah.
  3. v_total numeric; — tanpa nilai awal, variabel berisi NULL sampai diisi (perilaku ini dibedah di bagian terakhir).
  4. v_total_bayar = v_total * 0.9; — tanda sama dengan tunggal = juga diterima sebagai operator assignment. Gaya idiomatik komunitas adalah := supaya assignment tidak tertukar dengan pembanding = di ekspresi; sepanjang kurs ini kita memakai :=.

SELECT INTO: Mengisi Variabel dari Query #

Assignment := mengisi variabel dari ekspresi; SELECT INTO mengisinya dari query database — idiom terpenting dalam function yang membaca data:

sql select-into.sql
-- SELECT INTO: mengambil satu nilai dari query ke dalam variabel
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);

CREATE FUNCTION harga_produk(nama_dicari text)
RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
    v_harga numeric;
BEGIN
    SELECT p.harga INTO v_harga
    FROM produk p
    WHERE p.nama = nama_dicari;

    -- Tanpa baris, variabel tetap NULL: cek FOUND untuk menangani
    IF NOT FOUND THEN
        RAISE NOTICE 'Produk % tidak ada di daftar', nama_dicari;
        RETURN NULL;
    END IF;

    RETURN v_harga;
END
$$;

SELECT harga_produk('Kopi Gayo 250g') AS ketemu,
       harga_produk('Kopi Robusta')   AS kosong;

Output yang Dihasilkan:

text
CREATE TABLE
INSERT 0 3
CREATE FUNCTION
  ketemu  | kosong 
----------+--------
 85000.00 |       
(1 row)

Alurnya per baris:

  1. SELECT p.harga INTO v_harga FROM produk p WHERE ... — nilai kolom harga dari baris yang cocok disalin ke v_harga. Parameternya, nama_dicari, dipakai langsung di WHERE sebagai variabel.
  2. IF NOT FOUND THEN ... END IF — variabel khusus FOUND (boolean) mengingat apakah statement SQL terakhir menemukan baris. Ketika Kopi Robusta tidak ada, v_harga dibiarkan NULL dan cabang ini yang berjalan; function memutuskan responsnya sendiri — di sini RETURN NULL (pola IF ini dibahas tuntas di chapter Kondisi dan Perulangan).
  3. Hasil pemanggilan — kolom ketemu berisi 85000.00 untuk produk yang ada, kolom kosong kosong untuk yang tidak: NULL, bukan nol. (psql juga mencetak baris NOTICE: Produk Kopi Robusta tidak ada di daftar di terminal Anda — dari RAISE NOTICE di cabang NOT FOUND.)

SELECT INTO STRICT: Menuntut Tepat Satu Baris #

Menambahkan kata STRICT mengubah kontrak: nol baris atau lebih dari satu baris menjadi error yang membatalkan pemanggilan:

-- SELECT INTO STRICT: error bila hasil tidak tepat satu baris
CREATE FUNCTION harga_produk_strict(nama_dicari text)
RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
    v_harga numeric;
BEGIN
    SELECT p.harga INTO STRICT v_harga
    FROM produk p
    WHERE p.nama = nama_dicari;

    RETURN v_harga;
END
$$;

SELECT harga_produk_strict('Teh Melati 100g') AS harga_teh;

Output yang Dihasilkan:

text
CREATE FUNCTION
 harga_teh 
-----------
  15000.00
(1 row)

Versi function ini tidak lagi memerlukan pemeriksaan FOUND — ketika barisnya tidak ada, errornya langsung terjadi:

-- STRICT tanpa baris hasil: error P0002 langsung membatalkan pemanggilan
SELECT harga_produk_strict('Kopi Robusta');

Output yang Dihasilkan:

text
psql:select-into-strict-gagal.sql:2: ERROR:  query returned no rows
CONTEXT:  PL/pgSQL function harga_produk_strict(text) line 5 at SQL statement

Pesan query returned no rows berhenti di function tanpa menyentuh pemanggil berikutnya. Kapan memilih yang mana?

Kondisi hasil querySELECT INTOSELECT INTO STRICT
tepat satu barisvariabel terisivariabel terisi
nol barisvariabel NULL, FOUND = falseerror query returned no rows
lebih dari satu barisbaris pertama diambil diam-diamerror query returned more than one row
cocok untukdata opsional dengan fallbacklookup kunci yang wajib ada

Aturan praktisnya: data yang wajib ada (produk yang dirujuk faktur, pelanggan pemilik pesanan) pantas STRICT — salah cari berarti bug yang sebaiknya keras; data yang wajar hilang (nama promosi opsional) cukup polos dengan pemeriksaan NOT FOUND.

Jangkar %TYPE dan %ROWTYPE #

Deklarasi v_harga numeric punya kelemahan tersembunyi: bila suatu hari kolom produk.harga berganti tipe menjadi numeric(14,2), variabel kita tidak ikut berubah. PL/pgSQL menyediakan jangkar ke skema:

-- %TYPE dan %ROWTYPE: tipe variabel menaut ke definisi tabel
CREATE FUNCTION nilai_stok_produk(nama_dicari text)
RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
    v_harga produk.harga%TYPE;   -- persis tipe kolom produk.harga
    v_stok  produk.stok%TYPE;    -- persis tipe kolom produk.stok
    v_baris produk%ROWTYPE;      -- satu baris produk utuh
BEGIN
    -- INTO banyak kolom sekaligus, urutan menentukan
    SELECT p.harga, p.stok INTO v_harga, v_stok
    FROM produk p
    WHERE p.nama = nama_dicari;

    -- INTO satu variabel row: seluruh baris masuk
    SELECT p.* INTO v_baris
    FROM produk p
    WHERE p.nama = nama_dicari;

    RAISE NOTICE 'Produk id=% bernama %', v_baris.id, v_baris.nama;

    RETURN v_harga * v_stok;
END
$$;

SELECT nilai_stok_produk('Teh Melati 100g') AS nilai_teh,
       nilai_stok_produk('Kopi Gayo 250g')  AS nilai_kopi;

Output yang Dihasilkan:

text
CREATE FUNCTION
 nilai_teh  | nilai_kopi 
------------+------------
 1800000.00 | 3400000.00
(1 row)

Dua jenis jangkar bekerja di contoh itu:

  1. v_harga produk.harga%TYPE — tipe variabel disalin dari kolom harga milik tabel produk saat function dikompilasi. Tambahkan v_stok produk.stok%TYPE untuk pasangan harga-stok; SELECT INTO dengan banyak kolom mengisinya sekaligus — urutan kolom query harus sama dengan urutan variabel di INTO.
  2. v_baris produk%ROWTYPE — satu variabel menampung seluruh baris produk; SELECT p.* INTO v_baris mengisinya, dan anggotanya diakses dengan notasi titik: v_baris.id, v_baris.nama. (Penjelajahan record lebih dalam menunggu chapter Record dan Row Type.)

Ketika ALTER TABLE produk ALTER COLUMN harga TYPE numeric(14,2) dijalankan, PostgreSQL otomatis menandai function berjangkar %TYPE untuk dikompilasi ulang dengan tipe baru — function Anda ikut berubah tanpa disentuh. Jangkar juga menjadi dokumentasi hidup: pembaca langsung tahu variabel itu milik dunia produk.

NULL dan Ruang Lingkup Blok #

Dua perilaku terakhir sering menggigit: variabel yang belum diisi adalah NULL (bukan nol), dan blok bersarang punya aturan bayangan sendiri:

-- NULL pada variabel dan ruang lingkup blok bersarang
CREATE TABLE catatan_variabel (
    no    integer,
    pesan text
);

DO $$
DECLARE
    v_diskon numeric;            -- belum diisi: NULL
    v_total  numeric := 250000;
BEGIN
    INSERT INTO catatan_variabel
    VALUES (1, 'diskon awal: ' || coalesce(v_diskon::text, 'NULL'));

    -- NULL menjalar: 250000 + NULL tetap NULL
    INSERT INTO catatan_variabel
    VALUES (2, 'total + diskon: ' || coalesce((v_total + v_diskon)::text, 'NULL'));

    v_diskon := coalesce(v_diskon, 0);
    INSERT INTO catatan_variabel
    VALUES (3, 'setelah coalesce: ' || (v_total + v_diskon)::text);
END
$$;

DO $$
DECLARE
    v_harga numeric := 85000;    -- variabel milik blok luar
BEGIN
    INSERT INTO catatan_variabel VALUES (4, 'blok luar: ' || v_harga::text);

    DECLARE
        v_harga numeric := 15000;   -- menutupi (shadow) variabel luar
    BEGIN
        INSERT INTO catatan_variabel VALUES (5, 'blok dalam: ' || v_harga::text);
    END;

    INSERT INTO catatan_variabel VALUES (6, 'setelah blok dalam: ' || v_harga::text);
END
$$;

SELECT pesan FROM catatan_variabel ORDER BY no;

Output yang Dihasilkan:

text
CREATE TABLE
DO
DO
           pesan           
---------------------------
 diskon awal: NULL
 total + diskon: NULL
 setelah coalesce: 250000
 blok luar: 85000
 blok dalam: 15000
 setelah blok dalam: 85000
(6 rows)

Baris 1–3 membuktikan penjalaran NULL: v_diskon yang belum diisi membuat v_total + v_diskon bernilai NULL juga — bukan 250000, bukan error. Baris ke-3 menunjukkan penawarnya: coalesce(v_diskon, 0) mengganti NULL dengan nol sebelum berhitung. Baris 4–6 memperlihatkan variable shadowing: deklarasi v_harga di blok dalam menutupi variabel bernama sama milik blok luar (nilai 15000), dan begitu blok dalam berakhir, variabel luar muncul kembali dengan nilai aslinya 85000 — blok dalam tidak bisa mengubah variabel luar kecuali variabel itu memang tidak dideklarasikan ulang.

Kita kini bisa menyimpan, mengisi, dan menjaga data di dalam function. Yang masih statis adalah jalannya: kode berjalan lurus dari BEGIN ke END tanpa percabangan maupun pengulangan. Chapter berikutnya, Kondisi dan Perulangan, melengkapi bagian itu — IF, CASE, LOOP, WHILE, dan FOR yang mengubah function menjadi program sungguhan.

Referensi #

Berkas Sumber

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

deklarasi-assignment.sql

Unduh berkas
sql
-- Deklarasi variabel: nilai awal dengan := dan DEFAULT
CREATE TABLE hasil_hitung (
    langkah text,
    nilai   numeric
);

DO $$
DECLARE
    v_harga       numeric := 85000;   -- nilai awal lewat :=
    v_jumlah      integer DEFAULT 3;  -- DEFAULT: makna yang sama
    v_total       numeric;            -- tanpa nilai awal -> NULL
    v_total_bayar numeric;
BEGIN
    v_total := v_harga * v_jumlah;        -- assignment idiomatik
    v_total_bayar = v_total * 0.9;        -- '=' juga diterima

    INSERT INTO hasil_hitung VALUES ('total', v_total);
    INSERT INTO hasil_hitung VALUES ('total_bayar', v_total_bayar);
END
$$;

SELECT * FROM hasil_hitung ORDER BY langkah;

jangkar-type.sql

Unduh berkas
sql
-- %TYPE dan %ROWTYPE: tipe variabel menaut ke definisi tabel
CREATE FUNCTION nilai_stok_produk(nama_dicari text)
RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
    v_harga produk.harga%TYPE;   -- persis tipe kolom produk.harga
    v_stok  produk.stok%TYPE;    -- persis tipe kolom produk.stok
    v_baris produk%ROWTYPE;      -- satu baris produk utuh
BEGIN
    -- INTO banyak kolom sekaligus, urutan menentukan
    SELECT p.harga, p.stok INTO v_harga, v_stok
    FROM produk p
    WHERE p.nama = nama_dicari;

    -- INTO satu variabel row: seluruh baris masuk
    SELECT p.* INTO v_baris
    FROM produk p
    WHERE p.nama = nama_dicari;

    RAISE NOTICE 'Produk id=% bernama %', v_baris.id, v_baris.nama;

    RETURN v_harga * v_stok;
END
$$;

SELECT nilai_stok_produk('Teh Melati 100g') AS nilai_teh,
       nilai_stok_produk('Kopi Gayo 250g')  AS nilai_kopi;

null-dan-scope.sql

Unduh berkas
sql
-- NULL pada variabel dan ruang lingkup blok bersarang
CREATE TABLE catatan_variabel (
    no    integer,
    pesan text
);

DO $$
DECLARE
    v_diskon numeric;            -- belum diisi: NULL
    v_total  numeric := 250000;
BEGIN
    INSERT INTO catatan_variabel
    VALUES (1, 'diskon awal: ' || coalesce(v_diskon::text, 'NULL'));

    -- NULL menjalar: 250000 + NULL tetap NULL
    INSERT INTO catatan_variabel
    VALUES (2, 'total + diskon: ' || coalesce((v_total + v_diskon)::text, 'NULL'));

    v_diskon := coalesce(v_diskon, 0);
    INSERT INTO catatan_variabel
    VALUES (3, 'setelah coalesce: ' || (v_total + v_diskon)::text);
END
$$;

DO $$
DECLARE
    v_harga numeric := 85000;    -- variabel milik blok luar
BEGIN
    INSERT INTO catatan_variabel VALUES (4, 'blok luar: ' || v_harga::text);

    DECLARE
        v_harga numeric := 15000;   -- menutupi (shadow) variabel luar
    BEGIN
        INSERT INTO catatan_variabel VALUES (5, 'blok dalam: ' || v_harga::text);
    END;

    INSERT INTO catatan_variabel VALUES (6, 'setelah blok dalam: ' || v_harga::text);
END
$$;

SELECT pesan FROM catatan_variabel ORDER BY no;

select-into-strict-gagal.sql

Unduh berkas
sql
-- STRICT tanpa baris hasil: error P0002 langsung membatalkan pemanggilan
SELECT harga_produk_strict('Kopi Robusta');

select-into-strict.sql

Unduh berkas
sql
-- SELECT INTO STRICT: error bila hasil tidak tepat satu baris
CREATE FUNCTION harga_produk_strict(nama_dicari text)
RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
    v_harga numeric;
BEGIN
    SELECT p.harga INTO STRICT v_harga
    FROM produk p
    WHERE p.nama = nama_dicari;

    RETURN v_harga;
END
$$;

SELECT harga_produk_strict('Teh Melati 100g') AS harga_teh;

select-into.sql

Unduh berkas
sql
-- SELECT INTO: mengambil satu nilai dari query ke dalam variabel
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);

CREATE FUNCTION harga_produk(nama_dicari text)
RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
    v_harga numeric;
BEGIN
    SELECT p.harga INTO v_harga
    FROM produk p
    WHERE p.nama = nama_dicari;

    -- Tanpa baris, variabel tetap NULL: cek FOUND untuk menangani
    IF NOT FOUND THEN
        RAISE NOTICE 'Produk % tidak ada di daftar', nama_dicari;
        RETURN NULL;
    END IF;

    RETURN v_harga;
END
$$;

SELECT harga_produk('Kopi Gayo 250g') AS ketemu,
       harga_produk('Kopi Robusta')   AS kosong;