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:
CREATE TABLE
DO
langkah | nilai
-------------+----------
total | 255000
total_bayar | 229500.0
(2 rows)Bedah singkat polanya:
v_harga numeric := 85000;— deklarasi sekaligus pengisian: satu baris, tidak ada nilai NULL yang mengendap.v_jumlah integer DEFAULT 3;—DEFAULTidentik dengan:=di posisi ini; pilih satu gaya dan konsistenlah.v_total numeric;— tanpa nilai awal, variabel berisi NULL sampai diisi (perilaku ini dibedah di bagian terakhir).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:
-- 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:
CREATE TABLE
INSERT 0 3
CREATE FUNCTION
ketemu | kosong
----------+--------
85000.00 |
(1 row)Alurnya per baris:
SELECT p.harga INTO v_harga FROM produk p WHERE ...— nilai kolomhargadari baris yang cocok disalin kev_harga. Parameternya,nama_dicari, dipakai langsung diWHEREsebagai variabel.IF NOT FOUND THEN ... END IF— variabel khususFOUND(boolean) mengingat apakah statement SQL terakhir menemukan baris. KetikaKopi Robustatidak ada,v_hargadibiarkan NULL dan cabang ini yang berjalan; function memutuskan responsnya sendiri — di siniRETURN NULL(polaIFini dibahas tuntas di chapter Kondisi dan Perulangan).- Hasil pemanggilan — kolom
ketemuberisi85000.00untuk produk yang ada, kolomkosongkosong untuk yang tidak: NULL, bukan nol. (psql juga mencetak barisNOTICE: Produk Kopi Robusta tidak ada di daftardi terminal Anda — dariRAISE NOTICEdi cabangNOT 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:
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:
psql:select-into-strict-gagal.sql:2: ERROR: query returned no rows
CONTEXT: PL/pgSQL function harga_produk_strict(text) line 5 at SQL statementPesan query returned no rows berhenti di function tanpa menyentuh pemanggil berikutnya. Kapan memilih yang mana?
| Kondisi hasil query | SELECT INTO | SELECT INTO STRICT |
|---|---|---|
| tepat satu baris | variabel terisi | variabel terisi |
| nol baris | variabel NULL, FOUND = false | error query returned no rows |
| lebih dari satu baris | baris pertama diambil diam-diam | error query returned more than one row |
| cocok untuk | data opsional dengan fallback | lookup 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:
CREATE FUNCTION
nilai_teh | nilai_kopi
------------+------------
1800000.00 | 3400000.00
(1 row)Dua jenis jangkar bekerja di contoh itu:
v_harga produk.harga%TYPE— tipe variabel disalin dari kolomhargamilik tabelproduksaat function dikompilasi. Tambahkanv_stok produk.stok%TYPEuntuk pasangan harga-stok;SELECT INTOdengan banyak kolom mengisinya sekaligus — urutan kolom query harus sama dengan urutan variabel diINTO.v_baris produk%ROWTYPE— satu variabel menampung seluruh baris produk;SELECT p.* INTO v_barismengisinya, 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:
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-- 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-- %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-- 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-- STRICT tanpa baris hasil: error P0002 langsung membatalkan pemanggilan
SELECT harga_produk_strict('Kopi Robusta');
select-into-strict.sql
Unduh berkas-- 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-- 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;