Prosedur di chapter sebelumnya sudah mengolah nilai lewat parameter, tetapi perhitungan yang sedikit panjang butuh tempat kerja sendiri: hasil antara yang ditampung, penghitung yang bertambah, atau potongan harga yang dipakai berulang. MySQL menyediakan dua mekanisme penyimpanan nilai yang sering tertukar: variabel lokal yang dideklarasikan dengan DECLARE dan hidup hanya di dalam prosedur, serta variabel sesi yang ditulis dengan awalan @ dan hidup selama koneksi berlangsung. Pada chapter ini kita membedah keduanya — cara mengisi nilai, scope masing-masing, aturan urutan DECLARE yang ketat — lalu menutup dengan konvensi penamaan supaya kode prosedur mudah dibaca tim.
Menyiapkan Database Latihan #
Database belajar_sp dan tabel produk kita bangun ulang seperti biasa:
DROP DATABASE IF EXISTS belajar_sp;
CREATE DATABASE belajar_sp;
USE belajar_sp;Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changedCREATE TABLE produk (
id INT PRIMARY KEY,
nama VARCHAR(50) NOT NULL,
harga DECIMAL(10,2) NOT NULL,
stok INT NOT NULL
);
INSERT INTO produk (id, nama, harga, stok) VALUES
(1, 'Kopi Gayo', 55000.00, 10),
(2, 'Teh Melati', 25000.00, 20),
(3, 'Gula Aren', 15000.00, 30),
(4, 'Susu Kental', 8000.00, 40),
(5, 'Kopi Toraja', 62000.00, 5);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 5 rows affected (0.00 sec)
Records: 5 Duplicates: 0 Warnings: 0DECLARE: Variabel Lokal di dalam Prosedur #
Variabel lokal dideklarasikan dengan DECLARE nama tipe [DEFAULT nilai] dan hanya boleh hidup di dalam BEGIN ... END tempat ia dideklarasikan. Prosedur laporan_persediaan menghitung nilai persediaan lalu menyimpan selisihnya terhadap target:
DROP PROCEDURE IF EXISTS laporan_persediaan;
DELIMITER $$
CREATE PROCEDURE laporan_persediaan()
BEGIN
DECLARE v_potongan DECIMAL(10,2) DEFAULT 10000.00;
DECLARE v_total DECIMAL(12,2) DEFAULT 0.00;
DECLARE v_selisih DECIMAL(12,2);
SELECT SUM(harga * stok) INTO v_total FROM produk;
SET v_selisih = v_total - v_potongan;
SELECT v_total AS nilai_persediaan,
v_potongan AS potongan,
v_selisih AS selisih;
END$$
DELIMITER ;
CALL laporan_persediaan();Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
+------------------+----------+------------+
| nilai_persediaan | potongan | selisih |
+------------------+----------+------------+
| 2130000.00 | 10000.00 | 2120000.00 |
+------------------+----------+------------+
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec)Tiga variabel lokal bekerja sama di sini: v_potongan lahir dengan nilai awal 10000.00 dari clause DEFAULT, v_total lahir bernilai 0.00 lalu diisi hasil query, dan v_selisih lahir tanpa nilai awal (otomatis NULL) sebelum dihitung. Ada dua cara mengisi variabel yang terlihat pada contoh: SET v_selisih = ... untuk ekspresi bebas, dan SELECT ... INTO v_total untuk menampung hasil query — keduanya kita bedah tuntas di bagian berikutnya.
Urutan Deklarasi: DECLARE Selalu di Awal #
MySQL mensyaratkan seluruh DECLARE berdiri paling depan di dalam bloknya, sebelum statement lain apa pun. Deklarasi yang diletakkan setelah statement kerja langsung ditolak sebagai kesalahan sintaks:
-- @error
DELIMITER $$
CREATE PROCEDURE urutan_salah() BEGIN SELECT 1; DECLARE v_x INT; END$$
DELIMITER ;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'DECLARE v_x INT; END' at line 1
ByePesan ERROR 1064 menunjuk bagian DECLARE v_x INT; END sebagai wilayah yang salah — DECLARE sudah berada setelah SELECT 1, melanggar urutan yang diwajibkan. Aturan urutan lengkapnya pun berlapis: variabel dan kondisi dideklarasikan lebih dulu, disusul cursor, dan terakhir handler. Ketika sebuah prosedur butuh variabel baru di tengah alur, solusinya bukan menyelipkan DECLARE, melainkan membuka blok BEGIN ... END baru dan mendeklarasikan variabel di awal blok tersebut.
SET @var: Variabel Sesi Milik Koneksi #
Variabel sesi ditulis dengan awalan @, tidak perlu deklarasi, dan boleh dipakai langsung di statement SQL biasa — termasuk di luar prosedur. Nilainya bertahan selama koneksi yang sama masih terbuka:
SET @diskon = 0.10;
SELECT nama, harga, ROUND(harga * (1 - @diskon), 2) AS harga_setelah_diskon
FROM produk
ORDER BY id;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
+-------------+----------+----------------------+
| nama | harga | harga_setelah_diskon |
+-------------+----------+----------------------+
| Kopi Gayo | 55000.00 | 49500.00 |
| Teh Melati | 25000.00 | 22500.00 |
| Gula Aren | 15000.00 | 13500.00 |
| Susu Kental | 8000.00 | 7200.00 |
| Kopi Toraja | 62000.00 | 55800.00 |
+-------------+----------+----------------------+
5 rows in set (0.00 sec)Satu blok tersebut berjalan dalam satu koneksi: SET @diskon mengisi variabel, lalu SELECT di statement berikutnya masih membaca nilai yang sama. Namun begitu koneksi ditutup, variabel sesi ikut lenyap. Blok berikut memperlihatkan apa yang terjadi ketika @diskon dibaca dari koneksi baru:
SELECT @diskon AS diskon_dari_koneksi_baru;Output yang Dihasilkan:
+----------------------------------------------------+
| diskon_dari_koneksi_baru |
+----------------------------------------------------+
| NULL |
+----------------------------------------------------+
1 row in set (0.00 sec)Hasilnya NULL — koneksi baru tidak mewarisi variabel sesi koneksi lama. Perilaku ini penting diingat saat bekerja dengan aplikasi dan connection pool: variabel @ yang di-set pada satu request tidak selalu masih ada di request berikutnya, karena request itu bisa saja dilayani koneksi yang berbeda.
SELECT … INTO: Menampung Hasil Query #
SELECT daftar_kolom INTO daftar_variabel FROM ... menjalankan query lalu menyimpan hasilnya ke variabel alih-alih mengirimkannya ke klien. Prosedur info_produk memakai pola ini untuk mengambil satu baris produk:
DROP PROCEDURE IF EXISTS info_produk;
DELIMITER $$
CREATE PROCEDURE info_produk(IN p_id INT)
BEGIN
DECLARE v_nama VARCHAR(50);
DECLARE v_harga DECIMAL(10,2);
SELECT nama, harga INTO v_nama, v_harga
FROM produk WHERE id = p_id;
SELECT v_nama AS nama, v_harga AS harga, v_harga * 0.9 AS harga_coret;
END$$
DELIMITER ;
CALL info_produk(3);Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
+-----------+----------+-------------+
| nama | harga | harga_coret |
+-----------+----------+-------------+
| Gula Aren | 15000.00 | 13500.000 |
+-----------+----------+-------------+
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec)Query menemukan Gula Aren, nilai nama dan harga mengalir ke v_nama dan v_harga, lalu variabel tersebut dipakai lagi dalam ekspresi v_harga * 0.9 — bukti bahwa variabel hidup sebagai bagian dari ekspresi, bukan sekadar penampung sementara. SELECT ... INTO menuntut query berakhir dengan tepat satu baris: nol baris membuat variabel tetap NULL (dengan warning), sedangkan lebih dari satu baris ditolak saat dieksekusi. Prosedur berikut sengaja menampung kolom id dari seluruh tabel:
DROP PROCEDURE IF EXISTS into_banyak_baris;
DELIMITER $$
CREATE PROCEDURE into_banyak_baris()
BEGIN
DECLARE v_id INT;
SELECT id INTO v_id FROM produk;
END$$
DELIMITER ;Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 0 rows affected (0.00 sec)Pembuatan prosedur ini justru berhasil — pelanggaran jumlah baris SELECT ... INTO baru terdeteksi server ketika statement benar-benar dieksekusi, bukan saat prosedur dibuat. Kesalahan itu muncul begitu kita memanggilnya:
-- @error
CALL into_banyak_baris();Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1172 (42000): Result consisted of more than one row
ByeERROR 1172 menghentikan prosedur: lima baris produk tidak muat ke satu variabel v_id. Untuk data yang sah menghasilkan banyak baris, saring dengan kunci yang spesifik, batasi dengan LIMIT 1, atau bawa barisnya satu per satu lewat cursor — topik chapter Cursors.
Bedah Scope: DECLARE Lokal vs @ Sesi #
Perbedaan paling tajam antara kedua variabel adalah wilayah hidupnya (scope). Prosedur berikut sengaja mendeklarasikan v_dalam di blok BEGIN ... END bersarang, lalu mencoba membacanya dari blok luar:
DROP PROCEDURE IF EXISTS scope_demo;
DELIMITER $$
CREATE PROCEDURE scope_demo()
BEGIN
BEGIN
DECLARE v_dalam INT DEFAULT 7;
SELECT v_dalam AS nilai_dari_dalam;
END;
SELECT v_dalam AS nilai_dari_luar;
END$$
DELIMITER ;Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 0 rows affected (0.00 sec)Pembuatannya berhasil karena v_dalam sah di blok dalamnya. Masalah baru muncul saat pemanggilan:
-- @error
CALL scope_demo();Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
+------------------+
| nilai_dari_dalam |
+------------------+
| 7 |
+------------------+
1 row in set (0.00 sec)
ERROR 1054 (42S22): Unknown column 'v_dalam' in 'field list'
ByeHasil pertama 7 tercetak normal dari blok dalam, lalu ERROR 1054 menghentikan prosedur: v_dalam sudah mati saat blok dalamnya berakhir, sehingga nama tersebut tidak lagi dikenal — MySQL menganggapnya kolom yang tidak ada. Bandingkan dengan variabel sesi yang bisa dibaca dari mana saja selama masih satu koneksi, termasuk dari dalam prosedur:
SET @catatan = 'dibaca dari dalam prosedur';
DROP PROCEDURE IF EXISTS baca_sesi;
DELIMITER $$
CREATE PROCEDURE baca_sesi()
BEGIN
SELECT @catatan AS isi_variabel_sesi;
END$$
DELIMITER ;
CALL baca_sesi();Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
+----------------------------+
| isi_variabel_sesi |
+----------------------------+
| dibaca dari dalam prosedur |
+----------------------------+
1 row in set (0.00 sec)
Query OK, 0 rows affected (0.00 sec)Ringkasan perbedaan keduanya:
| Aspek | DECLARE v_x (lokal) | @x (sesi) |
|---|---|---|
| Tempat deklarasi | Awal blok BEGIN ... END | Tidak perlu deklarasi |
| Wilayah hidup | Blok tempat deklarasi saja | Seluruh koneksi, statement mana pun |
| Nilai awal | DEFAULT atau NULL | NULL |
| Hilang ketika | Blok berakhir | Koneksi ditutup |
| Tipe data | Ditulis eksplisit | Bebas, mengikuti nilai |
Konvensi Penamaan yang Praktis #
Ketika prosedur tumbuh panjang, nama variabel yang seragam menyelamatkan pembacaan. Konvensi yang umum dipakai: awalan v_ untuk variabel lokal, awalan p_ untuk parameter, awalan @ memang bawaan untuk variabel sesi, dan cur_ atau label baca:/ulang: untuk cursor serta loop yang menyusul di chapter berikutnya. Dengan pola ini, satu baris SET v_total = v_total + p_harga; langsung terbaca: variabel lokal bertambah sebesar parameter masukan.
Membersihkan Database Latihan #
DROP DATABASE belajar_sp;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)Referensi #
- DECLARE Local Variables — MySQL 8.4 Reference Manual
- SET Statement for Variable Assignment — MySQL 8.4 Reference Manual
- Compound Statement Syntax — MySQL 8.4 Reference Manual
Variabel lokal dan variabel sesi kini punya tempat masing-masing: DECLARE untuk pekerjaan di dalam prosedur dengan scope yang ketat, @ untuk nilai yang mengalir lintas statement dalam satu koneksi, dan SELECT ... INTO menjadi jembatan antara hasil query dan variabel. Perhitungan di prosedur kita pun bisa bertahap. Namun prosedur yang hanya berjalan lurus masih terasa datar — bab berikutnya menghadirkan percabangan IF, CASE, dan perulangan LOOP, WHILE, REPEAT yang membuat prosedur mampu mengambil keputusan dan mengulang pekerjaan.