Sebenarnya kita sudah memakai cursor sejak chapter Kondisi dan Perulangan — tanpa menyebut namanya. Setiap FOR rec IN SELECT ... LOOP membuka cursor implisit di belakang layar: PostgreSQL menjalankan query, menampung hasilnya, lalu mengalirkan baris satu per satu ke badan loop. Pola itu memenuhi 90% kebutuhan. Namun pekerjaan tertentu menuntut kendali lebih: laporan stok Toko Maju Jaya yang harus bisa berhenti di tengah jalan, pembaruan yang menargetkan baris yang sedang dipijak (bukan baris berdasar kondisi WHERE), atau query besar yang hasilnya ingin dibuka sekali lalu dioper ke pemanggil. Untuk semua itu PL/pgSQL menyediakan cursor eksplisit: dideklarasikan, dibuka dengan OPEN, dilangkahi dengan FETCH, ditutup dengan CLOSE — plus tipe refcursor untuk mengoper cursor antar function dan transaksi.
Cursor Eksplisit: OPEN, FETCH, CLOSE #
Siklus hidup cursor eksplisit lengkap: deklarasi CURSOR FOR di bagian DECLARE, OPEN untuk menjalankan query, FETCH ... INTO untuk mengambil satu baris, CLOSE untuk melepas hasilnya:
-- Cursor eksplisit: OPEN, FETCH INTO, CLOSE dengan patokan FOUND
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);
CREATE FUNCTION laporan_stok()
RETURNS SETOF text
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
cur CURSOR FOR SELECT id, nama, stok FROM produk ORDER BY id;
v_no integer := 0;
BEGIN
OPEN cur;
LOOP
FETCH cur INTO rec;
EXIT WHEN NOT FOUND;
v_no := v_no + 1;
RETURN NEXT format('%s. %s (stok %s)', v_no, rec.nama, rec.stok);
END LOOP;
CLOSE cur;
RETURN NEXT format('total: %s produk', v_no);
END
$$;
SELECT * FROM laporan_stok();Output yang Dihasilkan:
CREATE TABLE
INSERT 0 4
CREATE FUNCTION
laporan_stok
--------------------------------
1. Kopi Gayo 250g (stok 40)
2. Teh Melati 100g (stok 120)
3. Gula Kristal 1kg (stok 75)
4. Beras Premium 5kg (stok 25)
total: 4 produk
(5 rows)Bedah baris pentingnya:
cur CURSOR FOR SELECT id, nama, stok FROM produk ORDER BY id;— cursor terikat (bound) ke satu query sejak deklarasi. Struktur barisnya (id,nama,stok) mengikuti query itu — variabelrecrecord dari chapter Record dan Row Type pas menjadi wadahFETCH INTO.OPEN cur;— query baru benar-benar dieksekusi di titik ini; hasilnya disimpan server. SebelumOPEN, cursor belum berisi apa-apa.FETCH cur INTO rec; EXIT WHEN NOT FOUND;— satuFETCHmengambil satu baris. Variabel khususFOUND(yang juga dipakaiSELECT INTO) menjadi patokan loop: bernilai salah ketika baris habis, dan itulah pintu keluarnya.CLOSE cur;— melepas hasil di sisi server. Wajib untuk cursor eksplisit yang kita buka sendiri.
Cursor Berparameter #
Cursor boleh menerima parameter di deklarasinya — nilainya dikirim saat OPEN. Satu definisi query, banyak nilai:
-- Cursor berparameter: nilai dikirim saat OPEN, query tinggal sekali definisi
CREATE FUNCTION daftar_murah(batas numeric)
RETURNS SETOF text
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
cur CURSOR (batas_harga numeric) FOR
SELECT nama, harga FROM produk WHERE harga < batas_harga ORDER BY id;
BEGIN
OPEN cur(batas);
LOOP
FETCH cur INTO rec;
EXIT WHEN NOT FOUND;
RETURN NEXT rec.nama || ' - ' || rec.harga;
END LOOP;
CLOSE cur;
END
$$;
SELECT * FROM daftar_murah(50000);
SELECT * FROM daftar_murah(90000);Output yang Dihasilkan:
CREATE FUNCTION
daftar_murah
-----------------------------
Teh Melati 100g - 15000.00
Gula Kristal 1kg - 16000.00
(2 rows)
daftar_murah
------------------------------
Kopi Gayo 250g - 85000.00
Teh Melati 100g - 15000.00
Gula Kristal 1kg - 16000.00
Beras Premium 5kg - 68000.00
(4 rows)CURSOR (batas_harga numeric) FOR ... WHERE harga < batas_harga—batas_hargaadalah placeholder di definisi; nilainya baru diikat saatOPEN cur(batas)dengan argumen function.- Dua pemanggilan menghasilkan dua himpunan berbeda (2 produk di bawah Rp50.000, seluruh 4 produk di bawah Rp90.000) dari satu definisi cursor — pola yang membuat function penomoran/filter fleksibel tanpa query duplikat.
FOR over Cursor: Kendali Tanpa Administrasi #
Kedua dunia bisa digabung: FOR yang berputar di atas variabel cursor yang sudah dideklarasikan. OPEN, FETCH, dan CLOSE diurus otomatis, tetapi query tetap hidup di deklarasi — termasuk bentuk berparameternya:
-- FOR over cursor variable: OPEN, FETCH, dan CLOSE diurus otomatis
CREATE FUNCTION daftar_semua()
RETURNS SETOF text
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
cur CURSOR FOR SELECT nama, stok FROM produk ORDER BY id;
BEGIN
FOR rec IN cur LOOP
RETURN NEXT rec.nama || ' (stok ' || rec.stok || ')';
END LOOP;
END
$$;
SELECT * FROM daftar_semua();Output yang Dihasilkan:
CREATE FUNCTION
daftar_semua
-----------------------------
Kopi Gayo 250g (stok 40)
Teh Melati 100g (stok 120)
Gula Kristal 1kg (stok 75)
Beras Premium 5kg (stok 25)
(4 rows)FOR rec IN cur LOOP membuka cur, mengambil baris satu per satu ke rec, lalu menutupnya — hasilnya identik dengan loop FETCH manual di file pertama. Lantas kapan memilih yang mana?
| Aspek | FOR rec IN SELECT/cursor | OPEN + FETCH + CLOSE eksplisit |
|---|---|---|
| Jumlah baris kode | paling ringkas | paling panjang |
| Administrasi cursor | otomatis | manual (lupa CLOSE = hasil menggantung) |
| Keluar di tengah jalan | boleh (EXIT), cursor tetap ditutup | boleh — posisi baris tetap tercatat |
| Lanjut setelah loop (cek baris berikutnya, jeda antar baris) | tidak | ya — FETCH dipanggil kapan pun kita mau |
WHERE CURRENT OF | tidak cocok | bentuk aslinya |
Aturan praktisnya sederhana: mulai dengan FOR; turun ke FETCH manual hanya ketika alur benar-benar butuh kendali per-baris — misalnya memproses baris dari dua sumber bergantian, atau memutuskan antar-baris apakah loop harus berhenti.
WHERE CURRENT OF: Memperbarui Baris yang Dipijak #
Fitur cursor yang tidak dimiliki query biasa: UPDATE ... WHERE CURRENT OF cursor memperbarui baris yang terakhir di-FETCH — tanpa menyebut WHERE id = ... sama sekali:
-- WHERE CURRENT OF: memperbarui baris yang sedang dipijak cursor
CREATE FUNCTION sesuaikan_harga(persen numeric)
RETURNS SETOF text
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
v_baru numeric;
cur CURSOR FOR
SELECT id, nama, harga FROM produk WHERE stok < 100 ORDER BY id
FOR UPDATE;
BEGIN
OPEN cur;
LOOP
FETCH cur INTO rec;
EXIT WHEN NOT FOUND;
v_baru := round(rec.harga * (1 + persen / 100), 2);
UPDATE produk SET harga = v_baru WHERE CURRENT OF cur;
RETURN NEXT rec.nama || ': ' || rec.harga || ' menjadi ' || v_baru;
END LOOP;
CLOSE cur;
END
$$;
SELECT * FROM sesuaikan_harga(10);
SELECT id, nama, harga, stok FROM produk ORDER BY id;Output yang Dihasilkan:
CREATE FUNCTION
sesuaikan_harga
----------------------------------------------
Kopi Gayo 250g: 85000.00 menjadi 93500.00
Gula Kristal 1kg: 16000.00 menjadi 17600.00
Beras Premium 5kg: 68000.00 menjadi 74800.00
(3 rows)
id | nama | harga | stok
----+-------------------+----------+------
1 | Kopi Gayo 250g | 93500.00 | 40
2 | Teh Melati 100g | 15000.00 | 120
3 | Gula Kristal 1kg | 17600.00 | 75
4 | Beras Premium 5kg | 74800.00 | 25
(4 rows)WHERE CURRENT OF cur— positioned update: baris yang sedang dipijak diperbarui langsung. Tiga produk berganti harga (Kopi Rp85.000 → Rp93.500, Gula, Beras); Teh Melati tidak tersentung karena stoknya 120 tidak lolos filter cursor.FOR UPDATEpada query cursor — mengunci baris yang diambil sejakFETCHsampai transaksi selesai, sehingga sesi lain tidak bisa mengubahnya di antara pembacaan dan pembaruan. TanpaFOR UPDATE,WHERE CURRENT OFtetap berjalan, tetapi baris yang sudah berubah sejak dibaca bisa lolos dari pembaruan.
refcursor: Cursor untuk Pemanggil #
Semua cursor sejauh ini hidup dan mati di dalam satu function. Tipe refcursor mengubah cursor menjadi nilai yang bisa dioper: function membuka cursor bernama, mengembalikan namanya, dan pemanggil — psql, function lain, bahkan transaksi lain di sesi yang sama — mengambil barisnya lewat FETCH dengan nama itu:
-- refcursor: cursor bernama yang dioper ke pemanggil
CREATE FUNCTION buka_daftar_produk()
RETURNS refcursor
LANGUAGE plpgsql
AS $$
DECLARE
cur refcursor := 'daftar_produk';
BEGIN
OPEN cur FOR SELECT id, nama, stok FROM produk ORDER BY id;
RETURN cur;
END
$$;
BEGIN;
SELECT buka_daftar_produk();
FETCH ALL FROM "daftar_produk";
FETCH 2 FROM "daftar_produk";
COMMIT;Output yang Dihasilkan:
CREATE FUNCTION
BEGIN
buka_daftar_produk
--------------------
daftar_produk
(1 row)
id | nama | stok
----+-------------------+------
1 | Kopi Gayo 250g | 40
2 | Teh Melati 100g | 120
3 | Gula Kristal 1kg | 75
4 | Beras Premium 5kg | 25
(4 rows)
id | nama | stok
----+------+------
(0 rows)
COMMITcur refcursor := 'daftar_produk';— variabelrefcursoryang diisi string memberi nama portal cursor-nya; tanpa itu PostgreSQL mengarang nama otomatis<unnamed portal N>yang tidak bisa diprediksi.SELECT buka_daftar_produk();mengembalikan stringdaftar_produk— function hanya membuka; ia tidak mengirim satu baris pun ke pemanggil.FETCH ALL FROM "daftar_produk";— pemanggil mengambil seluruh baris dalam satu perintah, laluFETCH 2 FROMberikutnya mengembalikan nol baris karena cursor sudah habis. Pola ini untuk result set raksasa: pemanggil bisa mengambil sebagian, memproses, lalu mengambil sisa tanpa function menjalankan ulang query.
Cursor refcursor hidup selama transaksi yang membukanya. Di luar transaksi eksplisit — mode autocommit — statement pembuka sekaligus menutup cursor saat selesai:
-- Di luar transaksi, cursor ditutup otomatis saat statement pembuka selesai
SELECT buka_daftar_produk();
FETCH ALL FROM "daftar_produk";Output yang Dihasilkan:
psql:refcursor-tanpa-transaksi.sql:3: ERROR: cursor "daftar_produk" does not existError cursor "daftar_produk" does not exist membuktikan cursor sudah tertutup sebelum FETCH kesempatan berjalan — itulah sebabnya file sebelumnya membungkus sesinya dengan BEGIN ... COMMIT. Bila cursor memang harus melintasi batas transaksi, deklarasi SQL DECLARE ... WITH HOLD (saat commit, hasilnya disalin ke penyimpanan sementara) adalah jalurnya — opsi yang hanya tersedia pada cursor level SQL, bukan pada OPEN PL/pgSQL.
Kita kini memiliki seluruh perangkat stored program: function dengan segala bentuk hasil, exception yang tertangani rapi, procedure yang mengelola transaksinya sendiri, dan cursor untuk kendali per-baris. Pertanyaan terakhir bersifat berbeda — bukan bagaimana, melainkan kapan dan di mana: logika macam apa yang layak tinggal di dalam database, dan mana yang lebih baik ditinggal di kode aplikasi? Chapter penutup section ini menimbangnya secara jujur.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
cursor-dasar.sql
Unduh berkas-- Cursor eksplisit: OPEN, FETCH INTO, CLOSE dengan patokan FOUND
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);
CREATE FUNCTION laporan_stok()
RETURNS SETOF text
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
cur CURSOR FOR SELECT id, nama, stok FROM produk ORDER BY id;
v_no integer := 0;
BEGIN
OPEN cur;
LOOP
FETCH cur INTO rec;
EXIT WHEN NOT FOUND;
v_no := v_no + 1;
RETURN NEXT format('%s. %s (stok %s)', v_no, rec.nama, rec.stok);
END LOOP;
CLOSE cur;
RETURN NEXT format('total: %s produk', v_no);
END
$$;
SELECT * FROM laporan_stok();
cursor-parameter.sql
Unduh berkas-- Cursor berparameter: nilai dikirim saat OPEN, query tinggal sekali definisi
CREATE FUNCTION daftar_murah(batas numeric)
RETURNS SETOF text
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
cur CURSOR (batas_harga numeric) FOR
SELECT nama, harga FROM produk WHERE harga < batas_harga ORDER BY id;
BEGIN
OPEN cur(batas);
LOOP
FETCH cur INTO rec;
EXIT WHEN NOT FOUND;
RETURN NEXT rec.nama || ' - ' || rec.harga;
END LOOP;
CLOSE cur;
END
$$;
SELECT * FROM daftar_murah(50000);
SELECT * FROM daftar_murah(90000);
for-over-cursor.sql
Unduh berkas-- FOR over cursor variable: OPEN, FETCH, dan CLOSE diurus otomatis
CREATE FUNCTION daftar_semua()
RETURNS SETOF text
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
cur CURSOR FOR SELECT nama, stok FROM produk ORDER BY id;
BEGIN
FOR rec IN cur LOOP
RETURN NEXT rec.nama || ' (stok ' || rec.stok || ')';
END LOOP;
END
$$;
SELECT * FROM daftar_semua();
positioned-update.sql
Unduh berkas-- WHERE CURRENT OF: memperbarui baris yang sedang dipijak cursor
CREATE FUNCTION sesuaikan_harga(persen numeric)
RETURNS SETOF text
LANGUAGE plpgsql
AS $$
DECLARE
rec record;
v_baru numeric;
cur CURSOR FOR
SELECT id, nama, harga FROM produk WHERE stok < 100 ORDER BY id
FOR UPDATE;
BEGIN
OPEN cur;
LOOP
FETCH cur INTO rec;
EXIT WHEN NOT FOUND;
v_baru := round(rec.harga * (1 + persen / 100), 2);
UPDATE produk SET harga = v_baru WHERE CURRENT OF cur;
RETURN NEXT rec.nama || ': ' || rec.harga || ' menjadi ' || v_baru;
END LOOP;
CLOSE cur;
END
$$;
SELECT * FROM sesuaikan_harga(10);
SELECT id, nama, harga, stok FROM produk ORDER BY id;
refcursor-tanpa-transaksi.sql
Unduh berkas-- Di luar transaksi, cursor ditutup otomatis saat statement pembuka selesai
SELECT buka_daftar_produk();
FETCH ALL FROM "daftar_produk";
refcursor.sql
Unduh berkas-- refcursor: cursor bernama yang dioper ke pemanggil
CREATE FUNCTION buka_daftar_produk()
RETURNS refcursor
LANGUAGE plpgsql
AS $$
DECLARE
cur refcursor := 'daftar_produk';
BEGIN
OPEN cur FOR SELECT id, nama, stok FROM produk ORDER BY id;
RETURN cur;
END
$$;
BEGIN;
SELECT buka_daftar_produk();
FETCH ALL FROM "daftar_produk";
FETCH 2 FROM "daftar_produk";
COMMIT;