Function kita sejauh ini menampung data dalam variabel skalar — satu numeric untuk harga, satu text untuk nama. Padahal banyak pekerjaan kasir Toko Maju Jaya memproses satu baris produk utuh: ambil baris, periksa stoknya, hitung harga barunya, lalu tulis kembali. PL/pgSQL menyediakan dua wadah untuk satu baris penuh: record, variabel anonim yang strukturnya baru ditentukan saat runtime, dan row type (nama_tabel%ROWTYPE), yang strukturnya dikunci ke tabel sejak kompilasi. Keduanya mirip, tetapi perilaku keduanya berbeda penting — dan salah memilih bisa menghasilkan error yang membingungkan. Chapter ini membedah keduanya lewat SELECT INTO, loop FOR rec IN SELECT, sampai RETURN NEXT rec.
Variabel record: Struktur Baru Ditentukan Saat Runtime #
Kata kunci record mendeklarasikan variabel baris tanpa struktur apa pun. Baris kosong di DECLARE itu sungguh kosong: struktur barunya baru lahir ketika variabel diberi nilai — dari SELECT INTO, loop FOR, atau RETURN NEXT. Sebelum itu, variabel bernilai NULL, dan mencoba membaca field-nya memicu error record "rec" is not assigned yet.
-- Variabel record: struktur baris ditentukan saat runtime, bukan saat deklarasi
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 info_produk(v_id integer)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
BEGIN
SELECT * INTO rec FROM produk WHERE id = v_id;
IF NOT FOUND THEN
RETURN 'produk ' || v_id || ' tidak ditemukan';
END IF;
RETURN rec.nama || ': harga ' || rec.harga || ', stok ' || rec.stok;
END
$$;
SELECT info_produk(1) AS diproses,
info_produk(99) AS kosong;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 3
CREATE FUNCTION
diproses | kosong
-----------------------------------------+---------------------------
Kopi Gayo 250g: harga 85000.00, stok 40 | produk 99 tidak ditemukan
(1 row)Bedah baris pentingnya:
rec record;— deklarasi tanpa struktur. Berbeda dariintegeratautext, tiperecordtidak menyimpan satu nilai melainkan satu baris lengkap; kolom-kolomnya menyusul saat penugasan pertama.SELECT * INTO rec FROM produk WHERE id = v_id;— query dijalankan, dan bentuk kolom hasilnya (id,nama,harga,stok) menjadi strukturrec. Setelah baris ini,rec.nama,rec.harga, danrec.stoksah untuk dipakai.IF NOT FOUND THEN ... END IF;— pola pemeriksaan standar dari chapter Variables dan Assignments: saatSELECT INTOtidak menemukan baris,FOUNDbernilai salah danrecmenjadiNULL(bisa diuji denganrec IS NULL). Pemanggilan kedua membuktikannya:produk 99 tidak ditemukan.
FOR rec IN SELECT: Kebiasaan Utama Variabel record #
Tempat variabel record paling sering dipakai adalah loop FOR ... IN SELECT yang sudah kita kenal di chapter Kondisi dan Perulangan. Per putaran, rec menampung satu baris hasil query — struktur menempel dari query sumbernya:
-- FOR rec IN SELECT: memproses baris demi baris, lalu RETURN NEXT rec
CREATE FUNCTION nilai_stok()
RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
v_total numeric := 0;
BEGIN
FOR rec IN SELECT * FROM produk ORDER BY id LOOP
v_total := v_total + rec.harga * rec.stok;
END LOOP;
RETURN v_total;
END
$$;
CREATE FUNCTION produk_stok_rendah(batas integer)
RETURNS SETOF produk
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
BEGIN
FOR rec IN SELECT * FROM produk ORDER BY id LOOP
IF rec.stok <= batas THEN
RETURN NEXT rec;
END IF;
END LOOP;
END
$$;
SELECT nilai_stok() AS total_nilai_stok;
SELECT * FROM produk_stok_rendah(75);Output yang Dihasilkan:
CREATE FUNCTION
CREATE FUNCTION
total_nilai_stok
------------------
6400000.00
(1 row)
id | nama | harga | stok
----+------------------+----------+------
1 | Kopi Gayo 250g | 85000.00 | 40
3 | Gula Kristal 1kg | 16000.00 | 75
(2 rows)FOR rec IN SELECT * FROM produk ORDER BY id LOOP— setiap putaran mengisirecdengan satu baris produk utuh;rec.harga * rec.stokmengalikan dua field baris itu. Total6400000.00adalah nilai seluruh stok Toko Maju Jaya.RETURN NEXT rec;— satu baris penuh diserahkan ke hasil function. Karena function ini bertipeRETURNS SETOF produk, barisrecordyang anonim itu diperiksa dan dikonversi ke tipe barisproduksaat runtime. Hasilnya terlihat sebagai tabel dua baris: Kopi Gayo dan Gula Kristal yang stoknya tidak lebih dari 75.ORDER BY idpada query sumber tetap wajib — tanpa ia, urutan baris yang diproses (dan di-bufferRETURN NEXT) tidak terjamin.
Row Type dengan %ROWTYPE #
%ROWTYPE mendeklarasikan variabel baris yang strukturnya disalin dari tabel pada saat function dikompilasi: r produk%ROWTYPE artinya r selamanya punya empat field id, nama, harga, stok dengan tipe persis seperti kolom produk. Bila skema tabel berubah, field variabel ikut berubah — tidak perlu menyunting deklarasi:
-- %ROWTYPE: variabel baris yang strukturnya terikat ke tabel saat kompilasi
CREATE FUNCTION naikkan_harga(v_id integer, persen numeric)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
r produk%ROWTYPE;
BEGIN
SELECT * INTO r FROM produk WHERE id = v_id;
IF NOT FOUND THEN
RETURN 'produk ' || v_id || ' tidak ditemukan';
END IF;
UPDATE produk
SET harga = round(r.harga * (1 + persen / 100), 2)
WHERE id = r.id;
RETURN r.nama || ' kini dijual ' || round(r.harga * (1 + persen / 100), 2);
END
$$;
SELECT naikkan_harga(1, 10) AS kopi,
naikkan_harga(3, 5) AS gula,
naikkan_harga(99, 10) AS hilang;
SELECT id, nama, harga FROM produk ORDER BY id;Output yang Dihasilkan:
CREATE FUNCTION
kopi | gula | hilang
-------------------------------------+---------------------------------------+---------------------------
Kopi Gayo 250g kini dijual 93500.00 | Gula Kristal 1kg kini dijual 16800.00 | produk 99 tidak ditemukan
(1 row)
id | nama | harga
----+------------------+----------
1 | Kopi Gayo 250g | 93500.00
2 | Teh Melati 100g | 15000.00
3 | Gula Kristal 1kg | 16800.00
(3 rows)r produk%ROWTYPE;— variabelrmeminjam struktur tabelproduk: empat field bernama sama dengan kolomnya. Field juga bisa dipakai sebagai sumber nilai (r.harga) sekaligus kunci pembaruan (r.id) dalam satuUPDATE.SELECT * INTO rdengan query yang bentuknya persis tabel sumber adalah pasangan alami%ROWTYPE— semua kolom terisi ke field bernama sama, posisi demi posisi.- Pemanggilan ketiga (
99) menunjukkanNOT FOUNDbekerja sama persis seperti padarecord— baris tidak ada,rmenjadiNULL, function mengembalikan pesan sopan.
Satu record, Banyak Struktur — %ROWTYPE yang Kaku #
Kekuatan record sekaligus jadi risikonya: struktur boleh berganti-ganti dalam satu function. Demo berikut memakai satu variabel rec untuk dua query dengan kolom yang sama sekali berbeda:
-- Satu variabel record dipakai ulang untuk dua struktur baris yang berbeda
CREATE FUNCTION laporan_ganda()
RETURNS SETOF text
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
BEGIN
FOR rec IN SELECT nama, harga FROM produk ORDER BY id LOOP
RETURN NEXT 'harga: ' || rec.nama || ' = ' || rec.harga;
END LOOP;
FOR rec IN SELECT nama AS label, stok AS jumlah
FROM produk WHERE stok >= 75 ORDER BY id LOOP
RETURN NEXT 'stok : ' || rec.label || ' = ' || rec.jumlah;
END LOOP;
END
$$;
SELECT * FROM laporan_ganda();Output yang Dihasilkan:
CREATE FUNCTION
laporan_ganda
------------------------------------
harga: Kopi Gayo 250g = 93500.00
harga: Teh Melati 100g = 15000.00
harga: Gula Kristal 1kg = 16800.00
stok : Teh Melati 100g = 120
stok : Gula Kristal 1kg = 75
(5 rows)Loop pertama mengisi rec dengan dua kolom (nama, harga), loop kedua menimpanya dengan dua kolom lain (label, jumlah). Sama variabel, dua wajah — inilah makna harfiah “struktur ditentukan saat runtime”. Konsekuensinya juga nyata: begitu struktur berganti, field lama lenyap; membaca rec.stok setelah loop kedua akan memicu error record "rec" has no field "stok".
Bagaimana dengan %ROWTYPE yang diberi query berstruktur beda? %ROWTYPE tidak mau menyesuaikan diri — dan kegagalannya berbahaya karena senyap:
-- %ROWTYPE menolak query berstruktur beda: penugasan kolom bersifat posisi
CREATE FUNCTION baca_nama_salah(v_id integer)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
r produk%ROWTYPE;
BEGIN
SELECT p.nama, p.harga INTO r
FROM produk p
WHERE p.id = v_id;
RETURN r.nama;
END
$$;
SELECT baca_nama_salah(1);Output yang Dihasilkan:
psql:rowtype-kaku.sql:17: ERROR: invalid input syntax for type integer: "Kopi Gayo 250g"
CONTEXT: PL/pgSQL function baca_nama_salah(integer) line 5 at SQL statementError invalid input syntax for type integer: "Kopi Gayo 250g" terjadi karena penugasan ke %ROWTYPE bersifat posisi: kolom pertama hasil query (p.nama) diarahkan ke field pertama r (id bertipe integer), lalu teks "Kopi Gayo 250g" gagal dikonversi. %ROWTYPE tidak mengubah strukturnya mengikuti query — justru query yang dipaksa masuk ke struktur lama, kolom demi kolom, urutan demi urutan.
Perbandingan ringkas keduanya:
| Aspek | record | nama_tabel%ROWTYPE |
|---|---|---|
| Struktur | ditentukan saat runtime, bisa berganti | dikunci saat kompilasi, mengikuti tabel |
| Query berstruktur beda | diadopsi sebagai struktur baru | diisi posisi-per-posisi (rawan nilai nyasar) |
| Field tidak ada | error has no field saat akses | field tetap ada, ikut skema tabel |
| Cocok untuk | FOR IN SELECT kolom pilihan, hasil bentukan | menyalin/memindahkan baris tabel utuh |
| Ketika skema tabel berubah | aman bila field yang dipakai tetap ada | field otomatis ikut berubah |
Variabel record dan %ROWTYPE melengkapi perangkat manipulasi data kita. Namun semua contoh sejauh ini berjalan di dunia ideal: query selalu menemukan barisnya, stok tidak pernah negatif, email tidak pernah dobel. Database nyata penuh kegagalan — duplikat UNIQUE, pembagian nol, data hilang. Chapter berikutnya, Exception Handling, mengajarkan cara menangkapnya rapi dengan BEGIN ... EXCEPTION, membaca SQLERRM dan SQLSTATE, sampai melempar error buatan sendiri.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
for-select-record.sql
Unduh berkas-- FOR rec IN SELECT: memproses baris demi baris, lalu RETURN NEXT rec
CREATE FUNCTION nilai_stok()
RETURNS numeric
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
v_total numeric := 0;
BEGIN
FOR rec IN SELECT * FROM produk ORDER BY id LOOP
v_total := v_total + rec.harga * rec.stok;
END LOOP;
RETURN v_total;
END
$$;
CREATE FUNCTION produk_stok_rendah(batas integer)
RETURNS SETOF produk
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
BEGIN
FOR rec IN SELECT * FROM produk ORDER BY id LOOP
IF rec.stok <= batas THEN
RETURN NEXT rec;
END IF;
END LOOP;
END
$$;
SELECT nilai_stok() AS total_nilai_stok;
SELECT * FROM produk_stok_rendah(75);
record-dasar.sql
Unduh berkas-- Variabel record: struktur baris ditentukan saat runtime, bukan saat deklarasi
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 info_produk(v_id integer)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
BEGIN
SELECT * INTO rec FROM produk WHERE id = v_id;
IF NOT FOUND THEN
RETURN 'produk ' || v_id || ' tidak ditemukan';
END IF;
RETURN rec.nama || ': harga ' || rec.harga || ', stok ' || rec.stok;
END
$$;
SELECT info_produk(1) AS diproses,
info_produk(99) AS kosong;
record-fleksibel.sql
Unduh berkas-- Satu variabel record dipakai ulang untuk dua struktur baris yang berbeda
CREATE FUNCTION laporan_ganda()
RETURNS SETOF text
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
BEGIN
FOR rec IN SELECT nama, harga FROM produk ORDER BY id LOOP
RETURN NEXT 'harga: ' || rec.nama || ' = ' || rec.harga;
END LOOP;
FOR rec IN SELECT nama AS label, stok AS jumlah
FROM produk WHERE stok >= 75 ORDER BY id LOOP
RETURN NEXT 'stok : ' || rec.label || ' = ' || rec.jumlah;
END LOOP;
END
$$;
SELECT * FROM laporan_ganda();
rowtype-kaku.sql
Unduh berkas-- %ROWTYPE menolak query berstruktur beda: penugasan kolom bersifat posisi
CREATE FUNCTION baca_nama_salah(v_id integer)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
r produk%ROWTYPE;
BEGIN
SELECT p.nama, p.harga INTO r
FROM produk p
WHERE p.id = v_id;
RETURN r.nama;
END
$$;
SELECT baca_nama_salah(1);
rowtype.sql
Unduh berkas-- %ROWTYPE: variabel baris yang strukturnya terikat ke tabel saat kompilasi
CREATE FUNCTION naikkan_harga(v_id integer, persen numeric)
RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
r produk%ROWTYPE;
BEGIN
SELECT * INTO r FROM produk WHERE id = v_id;
IF NOT FOUND THEN
RETURN 'produk ' || v_id || ' tidak ditemukan';
END IF;
UPDATE produk
SET harga = round(r.harga * (1 + persen / 100), 2)
WHERE id = r.id;
RETURN r.nama || ' kini dijual ' || round(r.harga * (1 + persen / 100), 2);
END
$$;
SELECT naikkan_harga(1, 10) AS kopi,
naikkan_harga(3, 5) AS gula,
naikkan_harga(99, 10) AS hilang;
SELECT id, nama, harga FROM produk ORDER BY id;