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:

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

  1. rec record; — deklarasi tanpa struktur. Berbeda dari integer atau text, tipe record tidak menyimpan satu nilai melainkan satu baris lengkap; kolom-kolomnya menyusul saat penugasan pertama.
  2. SELECT * INTO rec FROM produk WHERE id = v_id; — query dijalankan, dan bentuk kolom hasilnya (id, nama, harga, stok) menjadi struktur rec. Setelah baris ini, rec.nama, rec.harga, dan rec.stok sah untuk dipakai.
  3. IF NOT FOUND THEN ... END IF; — pola pemeriksaan standar dari chapter Variables dan Assignments: saat SELECT INTO tidak menemukan baris, FOUND bernilai salah dan rec menjadi NULL (bisa diuji dengan rec 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:

text
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)
  1. FOR rec IN SELECT * FROM produk ORDER BY id LOOP — setiap putaran mengisi rec dengan satu baris produk utuh; rec.harga * rec.stok mengalikan dua field baris itu. Total 6400000.00 adalah nilai seluruh stok Toko Maju Jaya.
  2. RETURN NEXT rec; — satu baris penuh diserahkan ke hasil function. Karena function ini bertipe RETURNS SETOF produk, baris record yang anonim itu diperiksa dan dikonversi ke tipe baris produk saat runtime. Hasilnya terlihat sebagai tabel dua baris: Kopi Gayo dan Gula Kristal yang stoknya tidak lebih dari 75.
  3. ORDER BY id pada query sumber tetap wajib — tanpa ia, urutan baris yang diproses (dan di-buffer RETURN 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:

sql rowtype.sql
-- %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:

text
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)
  1. r produk%ROWTYPE; — variabel r meminjam struktur tabel produk: empat field bernama sama dengan kolomnya. Field juga bisa dipakai sebagai sumber nilai (r.harga) sekaligus kunci pembaruan (r.id) dalam satu UPDATE.
  2. SELECT * INTO r dengan query yang bentuknya persis tabel sumber adalah pasangan alami %ROWTYPE — semua kolom terisi ke field bernama sama, posisi demi posisi.
  3. Pemanggilan ketiga (99) menunjukkan NOT FOUND bekerja sama persis seperti pada record — baris tidak ada, r menjadi NULL, 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:

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

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

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

Aspekrecordnama_tabel%ROWTYPE
Strukturditentukan saat runtime, bisa bergantidikunci saat kompilasi, mengikuti tabel
Query berstruktur bedadiadopsi sebagai struktur barudiisi posisi-per-posisi (rawan nilai nyasar)
Field tidak adaerror has no field saat aksesfield tetap ada, ikut skema tabel
Cocok untukFOR IN SELECT kolom pilihan, hasil bentukanmenyalin/memindahkan baris tabel utuh
Ketika skema tabel berubahaman bila field yang dipakai tetap adafield 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- %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
sql
-- %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;