Prosedur kita sejauh ini berjalan lurus dari atas ke bawah — setiap statement selalu dieksekusi. Program yang sesungguhnya perlu mengambil keputusan (“jika stok menipis, kategorikan kritis”) dan mengulang pekerjaan (“jumlahkan dari 1 sampai N”). MySQL menyediakan keduanya di dalam stored program: percabangan lewat IF dan CASE statement, serta perulangan lewat LOOP, WHILE, dan REPEAT, lengkap dengan LEAVE untuk keluar dan ITERATE untuk melompat ke iterasi berikutnya. Pada chapter ini kita melatih keenamnya satu per satu dengan contoh kecil yang bisa dihitung ulang secara manual, lalu menutup dengan perbandingan kapan masing-masing bentuk perulangan paling pas dipakai.

Menyiapkan Database Latihan #

Database dan tabel latihan dibangun ulang seperti biasa:

sql
DROP DATABASE IF EXISTS belajar_sp;
CREATE DATABASE belajar_sp;
USE belajar_sp;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Database changed
sql
CREATE 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:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 5 rows affected (0.00 sec)
Records: 5  Duplicates: 0  Warnings: 0

IF … THEN … ELSEIF … ELSE … END IF #

IF statement mengevaluasi kondisi dan memilih cabang pertama yang bernilai benar. Prosedur kategori_harga menerima satu harga lalu mengembalikan label kelasnya:

sql
DROP PROCEDURE IF EXISTS kategori_harga;
DELIMITER $$
CREATE PROCEDURE kategori_harga(IN p_harga DECIMAL(10,2))
BEGIN
  DECLARE v_label VARCHAR(20);

  IF p_harga >= 50000 THEN
    SET v_label = 'premium';
  ELSEIF p_harga >= 20000 THEN
    SET v_label = 'menengah';
  ELSE
    SET v_label = 'ekonomis';
  END IF;

  SELECT v_label AS kategori;
END$$
DELIMITER ;
CALL kategori_harga(55000.00);
CALL kategori_harga(25000.00);
CALL kategori_harga(8000.00);

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+----------+
| kategori |
+----------+
| premium  |
+----------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+----------+
| kategori |
+----------+
| menengah |
+----------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+----------+
| kategori |
+----------+
| ekonomis |
+----------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Alurnya bergaya tangga: 55000.00 memenuhi kondisi pertama sehingga labelnya premium; 25000.00 gagal di cabang pertama namun lolos di ELSEIF kedua sehingga labelnya menengah; 8000.00 tidak memenuhi keduanya dan jatuh ke ELSE sebagai ekonomis. Urutan cabang menentukan hasil — 25000.00 tidak pernah mencapai ELSE karena ELSEIF sudah menangkapnya lebih dulu. ELSE boleh ditiadakan, tetapi bila tidak ada cabang yang cocok, prosedur lanjut tanpa mengubah apa pun.

CASE Statement: Percabangan Berbasis Daftar Kondisi #

CASE statement menawarkan gaya penulisan yang lebih rapi ketika cabangnya banyak. Prosedur label_stok mengubah nama variabel menjadi daftar kondisi berpola WHEN kondisi THEN aksi:

sql
DROP PROCEDURE IF EXISTS label_stok;
DELIMITER $$
CREATE PROCEDURE label_stok(IN p_stok INT)
BEGIN
  DECLARE v_label VARCHAR(20);

  CASE
    WHEN p_stok >= 30 THEN SET v_label = 'aman';
    WHEN p_stok >= 10 THEN SET v_label = 'menipis';
    ELSE SET v_label = 'kritis';
  END CASE;

  SELECT v_label AS status_stok;
END$$
DELIMITER ;
CALL label_stok(30);
CALL label_stok(20);
CALL label_stok(5);

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+-------------+
| status_stok |
+-------------+
| aman        |
+-------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+-------------+
| status_stok |
+-------------+
| menipis     |
+-------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+-------------+
| status_stok |
+-------------+
| kritis      |
+-------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Ketiga pemanggilan menghasilkan aman, menipis, dan kritis — perilaku yang persis setara IF bertangga, hanya ditulis dalam format daftar. Perlu dibedakan dari CASE ekspresi yang pernah kita pakai di chapter Fungsi Kondisional: CASE ekspresi hidup di dalam SELECT dan menghasilkan nilai (misalnya SELECT CASE WHEN ... THEN ... END), sedangkan CASE statement berdiri sendiri sebagai statement penuh dan bagian THEN-nya berisi statement kerja seperti SET. Keduanya tidak bisa saling menggantikan.

Kita buat prosedur yang sama tanpa ELSE, lalu panggil dengan nilai yang tidak mengenai satu cabang pun:

sql
DROP PROCEDURE IF EXISTS label_tanpa_else;
DELIMITER $$
CREATE PROCEDURE label_tanpa_else(IN p_stok INT)
BEGIN
  DECLARE v_label VARCHAR(20);

  CASE
    WHEN p_stok >= 30 THEN SET v_label = 'aman';
  END CASE;

  SELECT v_label AS status_stok;
END$$
DELIMITER ;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Prosedur berhasil dibuat, karena pemeriksaan cabang terjadi saat eksekusi. Sekarang kita panggil dengan 5:

sql
-- @error
CALL label_tanpa_else(5);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1339 (20000): Case not found for CASE statement
Bye

ERROR 1339 (20000): Case not found for CASE statement menghentikan prosedur di tempat. Inilah alasan CASE statement hampir selalu ditulis dengan ELSE — meski isinya hanya SET v_label = NULL; sebagai jaring pengaman.

LOOP dan LEAVE: Perulangan dengan Label #

LOOP adalah perulangan tanpa kondisi bawaan: bloknya berulang terus sampai kita menyuruhnya berhenti lewat LEAVE. Karena itu setiap LOOP bermakna praktis selalu diberi label dan berisi pemeriksaan kondisi keluar. Prosedur total_sampai menjumlahkan bilangan 1 sampai N:

sql
DROP PROCEDURE IF EXISTS total_sampai;
DELIMITER $$
CREATE PROCEDURE total_sampai(IN p_n INT)
BEGIN
  DECLARE v_i INT DEFAULT 1;
  DECLARE v_total BIGINT DEFAULT 0;

  ulang: LOOP
    IF v_i > p_n THEN
      LEAVE ulang;
    END IF;
    SET v_total = v_total + v_i;
    SET v_i = v_i + 1;
  END LOOP ulang;

  SELECT v_total AS jumlah_deret;
END$$
DELIMITER ;
CALL total_sampai(10);

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+--------------+
| jumlah_deret |
+--------------+
|           55 |
+--------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Hasilnya 55 — sesuai hitungan manual 1+2+…+10. Mari kita bedah komponen kuncinya: label ulang: menandai pintu masuk loop dan menjadi alamat tujuan LEAVE ulang; variabel v_i berperan sebagai penghitung yang bertambah tiap putaran; dan pemeriksaan IF v_i > p_n di awal blok adalah satu-satunya pintu keluar — tanpa baris itu, prosedur mengulang tanpa akhir dan hanya bisa dihentikan dari luar. Label juga ditulis ulang di END LOOP ulang sebagai penutup yang eksplisit, membuat blok mudah dikenali saat loop bersarang.

WHILE … DO: Kondisi Dicek Sebelum Iterasi #

WHILE memindahkan pemeriksaan kondisi ke depan: selama kondisi bernilai benar, blok dijalankan. Prosedur faktorial memakai pola ini untuk menghitung n!:

sql
DROP PROCEDURE IF EXISTS faktorial;
DELIMITER $$
CREATE PROCEDURE faktorial(IN p_n INT, OUT p_hasil BIGINT)
BEGIN
  DECLARE v_i INT DEFAULT 1;

  SET p_hasil = 1;
  WHILE v_i <= p_n DO
    SET p_hasil = p_hasil * v_i;
    SET v_i = v_i + 1;
  END WHILE;
END$$
DELIMITER ;
CALL faktorial(5, @f5);
SELECT @f5 AS faktorial_5;
CALL faktorial(10, @f10);
SELECT @f10 AS faktorial_10;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+-------------+
| faktorial_5 |
+-------------+
|         120 |
+-------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+--------------+
| faktorial_10 |
+--------------+
|      3628800 |
+--------------+
1 row in set (0.00 sec)

5! menghasilkan 120 dan 10! menghasilkan 3628800. Perhatikan wujud alurnya: WHILE v_i <= p_n DO dievaluasi sebelum tiap putaran, sehingga bila p_n bernilai 0, kondisi langsung salah pada pemeriksaan pertama dan blok tidak pernah dieksekusi — p_hasil tetap 1, yang kebetulan juga nilai 0!. Sifat “nol iterasi pun aman” inilah keunggulan utama WHILE dibanding REPEAT.

REPEAT … UNTIL: Kondisi Dicek Setelah Iterasi #

REPEAT membalik posisinya: blok dijalankan dulu, kondisi keluar diperiksa kemudian lewat UNTIL. Prosedur total_sampai_repeat menghitung deret yang sama dengan gaya penulisan berbeda:

sql
DROP PROCEDURE IF EXISTS total_sampai_repeat;
DELIMITER $$
CREATE PROCEDURE total_sampai_repeat(IN p_n INT)
BEGIN
  DECLARE v_i INT DEFAULT 1;
  DECLARE v_total BIGINT DEFAULT 0;

  REPEAT
    SET v_total = v_total + v_i;
    SET v_i = v_i + 1;
  UNTIL v_i > p_n END REPEAT;

  SELECT v_total AS jumlah_deret;
END$$
DELIMITER ;
CALL total_sampai_repeat(10);

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+--------------+
| jumlah_deret |
+--------------+
|           55 |
+--------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Hasilnya kembali 55 — algoritma sama, bentuk berbeda. Pembeda perilakunya terletak pada iterasi pertama: REPEAT menjamin blok berjalan minimal satu kali karena kondisi UNTIL v_i > p_n baru diperiksa setelah blok selesai. Bila p_n kita isi 0, versi WHILE langsung berhenti dengan total 0, sedangkan versi REPEAT tetap menjalankan blok sekali dan menghasilkan total 1 — pilihan bentuk loop jadi keputusan desain, bukan selera.

Perbandingan ketiganya dirangkum berikut:

BentukPosisi cek kondisiIterasi minimumCocok untuk
LOOP + LEAVEmanual di dalam blok0 (bila cek di depan)alur kompleks dengan beberapa titik keluar
WHILEsebelum blok0iterasi yang mungkin tidak perlu jalan
REPEATsetelah blok1proses yang sekali jalan dulu baru dinilai

ITERATE: Melompat ke Iterasi Berikutnya #

ITERATE label melewatkan sisa blok dan langsung melompat ke awal iterasi — saudara continue pada bahasa pemrograman lain. Prosedur jumlah_genap menjumlahkan hanya bilangan genap dengan cara melewati bilangan ganjil:

sql
DROP PROCEDURE IF EXISTS jumlah_genap;
DELIMITER $$
CREATE PROCEDURE jumlah_genap(IN p_n INT)
BEGIN
  DECLARE v_i INT DEFAULT 0;
  DECLARE v_total BIGINT DEFAULT 0;

  ulang: LOOP
    SET v_i = v_i + 1;
    IF v_i > p_n THEN
      LEAVE ulang;
    END IF;
    IF v_i MOD 2 = 1 THEN
      ITERATE ulang;
    END IF;
    SET v_total = v_total + v_i;
  END LOOP ulang;

  SELECT v_total AS jumlah_bilangan_genap;
END$$
DELIMITER ;
CALL jumlah_genap(10);

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+-----------------------+
| jumlah_bilangan_genap |
+-----------------------+
|                    30 |
+-----------------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Hasilnya 30 — penjumlahan 2+4+6+8+10. Urutan statement dalam blok ini penting: penghitung v_i dinaikkan paling awal agar ITERATE yang melompat ke label tetap membawa kemajuan; bila kenaikan penghitung diletakkan di baris paling akhir, ITERATE akan melompat tanpa pernah memajukan v_i dan prosedur berputar selamanya. LEAVE dan ITERATE sama-sama membutuhkan label, sehingga loop tanpa label tidak bisa memakai keduanya.

Akumulasi Data Tabel dengan Loop #

Sebagai penutup, kita gabungkan SELECT ... INTO, IF, dan REPEAT untuk satu tugas bersumber data: menghitung nilai persediaan harga * stok setiap produk lalu menggolongkan totalnya. Loop di sini menunjuk baris lewat id satu per satu tanpa cursor — cukup untuk data kecil dan menunjukkan pola akumulasi:

sql
DROP PROCEDURE IF EXISTS persediaan_rata;
DELIMITER $$
CREATE PROCEDURE persediaan_rata(OUT p_total DECIMAL(12,2))
BEGIN
  DECLARE v_id INT DEFAULT 1;
  DECLARE v_jumlah INT;
  DECLARE v_baris INT DEFAULT 0;
  DECLARE v_total DECIMAL(12,2) DEFAULT 0;

  SET p_total = 0;
  SELECT COUNT(*) INTO v_jumlah FROM produk;

  REPEAT
    SELECT SUM(harga * stok) INTO v_total FROM produk WHERE id = v_id;
    IF v_total IS NOT NULL THEN
      SET p_total = p_total + v_total;
      SET v_baris = v_baris + 1;
    END IF;
    SET v_id = v_id + 1;
  UNTIL v_id > v_jumlah END REPEAT;

  SELECT v_baris AS produk_dihitung;
END$$
DELIMITER ;
CALL persediaan_rata(@total_semua);
SELECT @total_semua AS nilai_persediaan;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+-----------------+
| produk_dihitung |
+-----------------+
|               5 |
+-----------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+------------------+
| nilai_persediaan |
+------------------+
|       2130000.00 |
+------------------+
1 row in set (0.00 sec)

Lima produk terakumulasi menjadi 2130000.00 — angka yang sama dengan SELECT SUM(harga * stok) FROM produk, bukti loop kita menghitung dengan benar. Perhatikan kombinasi polanya: SELECT ... INTO mengambil nilai per baris, IF v_total IS NOT NULL menjaga baris kosong tetap dilewati, dan REPEAT ... UNTIL v_id > v_jumlah mengendalikan putaran. Satu baris SET p_total = 0; juga bukan hiasan: parameter OUT selalu dimulai dari NULL, dan operasi penjumlahan yang berawal dari NULL akan tetap NULL sampai akhir — angka 2130000.00 di atas hanya muncul karena akumulator kita dinolkan lebih dulu. Pola penunjuk-baris seperti ini praktis untuk tabel kecil; untuk tabel besar, membaca baris berurutan sekali jalan lewat cursor jauh lebih efisien — itulah topik chapter berikutnya.

Membersihkan Database Latihan #

sql
DROP DATABASE belajar_sp;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

Referensi #

Percabangan dan perulangan kini lengkap kita kuasai: IF dan CASE statement mengambil keputusan, LOOP dengan LEAVE serta WHILE dan REPEAT mengulang pekerjaan, dan ITERATE menyaring iterasi. Namun contoh akhir chapter ini masih membaca tabel baris demi baris lewat query terpisah — mahal untuk data besar. Chapter berikutnya memperkenalkan cursor: mekanisme bawaan MySQL untuk menelusuri hasil query satu baris setiap FETCH secara berurutan dan efisien di dalam prosedur.