Sepanjang course ini kita selalu menulis query sebagai perintah sekali jalan: mengirim teks SQL dari klien ke server, menerima hasilnya, lalu selesai. Aplikasi nyata sering menjalankan rangkaian query yang sama berulang kali — rekap penjualan tiap pagi, validasi stok tiap transaksi, laporan bulanan — dan mengirim ulang rangkaian panjang itu dari aplikasi setiap kali terasa boros. Stored procedure menjawab masalah tersebut: sebuah program bernama yang disimpan permanen di dalam database, berisi satu atau banyak statement SQL, dan dijalankan kapan pun cukup dengan satu perintah CALL. Pada chapter ini kita membuat prosedur pertama, memahami mengapa perintah DELIMITER wajib hadir, membuktikan bahwa satu prosedur bisa memuat banyak statement, lalu memeriksa daftar prosedur yang tersimpan di server.

Prosedur: Program yang Tinggal di dalam Server #

Stored procedure adalah objek database — sama seperti tabel dan view — yang isinya bukan data, melainkan kode SQL siap pakai. Setelah CREATE PROCEDURE berhasil, server menyimpan teks prosedur beserta metadata-nya di dalam database; prosedur tetap ada walau klien menutup koneksi, dan bisa dipanggil oleh klien mana pun yang memiliki hak akses. Perbedaan mendasarnya dengan query biasa dapat kita ringkas begini:

AspekQuery biasaStored procedure
Tempat tinggalHanya ada di aplikasi/klienTersimpan di server database
Cara menjalankanKirim teks SQL lengkapCALL nama_prosedur()
IsiSatu statement (umumnya)Bisa banyak statement berurutan
Kompilasi ulangSetiap kali dikirimSekali saat dibuat

Karena prosedur hidup di dalam database, seluruh latihan kita memakai database latihan belajar_sp dengan tabel produk yang sama 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

Mengapa Kita Membutuhkan DELIMITER #

Sebelum menulis prosedur pertama, kita perlu memahami satu rintangan kecil. Klien mysql menganggap tanda titik koma (;) sebagai akhir sebuah statement: begitu ; ditemukan, teks yang terkumpul dikirim ke server untuk dieksekusi. Masalahnya, tubuh prosedur justru membutuhkan banyak ; di dalamnya — satu untuk tiap statement di dalam BEGIN ... END. Jika kita menulis CREATE PROCEDURE begitu saja, klien akan memotong perintah di titik koma pertama dan mengirim potongan yang tidak utuh, sehingga server menolaknya sebagai kesalahan sintaks.

Solusinya adalah perintah DELIMITER: kita mengganti penanda akhir statement dari ; menjadi $$ selama proses pembuatan prosedur, lalu mengembalikannya ke ; setelah prosedur jadi. Pola lengkapnya selalu berbentuk satu rangkaian utuh:

text
DROP PROCEDURE IF EXISTS nama;      -- bersihkan versi lama
DELIMITER $$                        -- ganti penanda akhir ke $$
CREATE PROCEDURE nama()
BEGIN
  ... statement dengan ; bebas ...
END$$                               -- akhir statement: $$
DELIMITER ;                         -- kembalikan penanda ke ;

Prosedur Pertama: halo() #

Kita mulai dari prosedur paling sederhana: tanpa parameter, dengan satu statement SELECT di dalam tubuhnya:

sql
DROP PROCEDURE IF EXISTS halo;
DELIMITER $$
CREATE PROCEDURE halo()
BEGIN
  SELECT 'Halo dari stored procedure' AS pesan;
END$$
DELIMITER ;
CALL halo();

Output yang Dihasilkan:

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

Query OK, 0 rows affected (0.00 sec)

+----------------------------+
| pesan                      |
+----------------------------+
| Halo dari stored procedure |
+----------------------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Prosedur halo berhasil dibuat lalu langsung dipanggil dan mengembalikan satu baris hasil. Mari kita bedah rangkaian di atas baris per baris:

  1. DROP PROCEDURE IF EXISTS halo; menghapus prosedur halo bila sebelumnya sudah ada. Baris ini ditulis dengan ; karena penanda akhir statement masih default. Menjalankan blok ini dua kali tidak akan pernah gagal — pola idempoten yang akan kita bahas di bagian akhir.
  2. DELIMITER $$ memberi tahu klien: mulai sekarang sebuah statement hanya dianggap selesai ketika $$ ditemukan, bukan ;.
  3. CREATE PROCEDURE halo() mendeklarasikan prosedur bernama halo tanpa parameter — ditandai sepasang kurung kosong. Nama prosedur harus unik di dalam database.
  4. BEGIN … END membentuk tubuh prosedur (compound statement). Semua statement di antara keduanya menjadi isi prosedur dan dijalankan berurutan setiap kali prosedur dipanggil. Tanda ; di baris SELECT sudah aman karena penanda akhir sekarang adalah $$.
  5. END$$ menutup tubuh sekaligus menandai akhir statement CREATE PROCEDURE — perintah utuh kini dikirim ke server.
  6. DELIMITER ; mengembalikan penanda akhir statement ke titik koma, supaya statement berikutnya (termasuk CALL) kembali ditulis secara normal.
  7. CALL halo(); menjalankan prosedur. Server membaca definisi tersimpan, mengeksekusi seluruh tubuhnya, lalu mengirim hasil SELECT kepada kita sebagai result set.

Prosedur dengan Query Nyata #

Prosedur menjadi berguna ketika tubuhnya memuat query yang benar-benar kita pakai sehari-hari. Kita buat prosedur daftar_produk yang mengembalikan isi tabel produk:

sql
DROP PROCEDURE IF EXISTS daftar_produk;
DELIMITER $$
CREATE PROCEDURE daftar_produk()
BEGIN
  SELECT id, nama, harga, stok FROM produk ORDER BY id;
END$$
DELIMITER ;
CALL daftar_produk();

Output yang Dihasilkan:

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

Query OK, 0 rows affected (0.00 sec)

+----+-------------+----------+------+
| id | nama        | harga    | stok |
+----+-------------+----------+------+
|  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 |
+----+-------------+----------+------+
5 rows in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Hasilnya identik dengan menjalankan SELECT secara langsung, namun kini aplikasi cukup mengirim CALL daftar_produk(); — teks query beserta logika pengurutannya tinggal di server. Kalau suatu saat kolom perlu ditambah atau urutan diubah, kita cukup memperbarui prosedurnya sekali dan semua pemanggil ikut berubah.

Tubuh Multi-Statement di dalam BEGIN … END #

Kekuatan sesungguhnya muncul ketika satu prosedur memuat banyak statement. Prosedur berikut mengembalikan tiga result sekaligus dalam satu kali CALL: jumlah produk, rata-rata harga, dan nilai persediaan:

sql
DROP PROCEDURE IF EXISTS ringkasan;
DELIMITER $$
CREATE PROCEDURE ringkasan()
BEGIN
  SELECT COUNT(*) AS jumlah_produk FROM produk;
  SELECT ROUND(AVG(harga), 2) AS rata_rata_harga FROM produk;
  SELECT SUM(harga * stok) AS nilai_persediaan FROM produk;
END$$
DELIMITER ;
CALL ringkasan();

Output yang Dihasilkan:

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

Query OK, 0 rows affected (0.00 sec)

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

+-----------------+
| rata_rata_harga |
+-----------------+
|        33000.00 |
+-----------------+
1 row in set (0.00 sec)

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

Query OK, 0 rows affected (0.00 sec)

Satu perintah CALL memicu tiga query berurutan, dan ketiga hasilnya sampai ke klien sebagai tiga result set terpisah. Tanpa prosedur, aplikasi harus mengirim tiga query berturut-turut — tiga kali bolak-balik jaringan; dengan prosedur, aplikasi cukup sekali panggil. Inilah inti “program tersimpan di server”: logika berada dekat dengan data.

Membuat Ulang Prosedur Secara Idempoten #

Nama prosedur harus unik di dalam satu database. Jika kita menjalankan CREATE PROCEDURE untuk nama yang sudah ada, server menolaknya:

sql
-- @error
DELIMITER $$
CREATE PROCEDURE daftar_produk() BEGIN SELECT 1; END$$
DELIMITER ;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1304 (42000): PROCEDURE daftar_produk already exists
Bye

Server membalas ERROR 1304 karena daftar_produk sudah terdaftar. Perhatikan bahwa tubuh satu baris (BEGIN SELECT 1; END) tetap sah — penanda ; di dalamnya diabaikan klien selama penanda akhir aktif adalah $$. Karena latihan kita sering menjalankan ulang blok yang sama, setiap pembuatan prosedur di chapter ini selalu diawali DROP PROCEDURE IF EXISTS nama;. Pola ini membuat rangkaian latihan idempoten: dieksekusi sekali pun aman, dieksekusi sepuluh kali pun hasilnya konsisten.

Melihat Prosedur yang Sudah Dibuat #

Ada tiga cara memeriksa prosedur yang tersimpan. Pertama, SHOW PROCEDURE STATUS menampilkan seluruh prosedur pada satu database:

sql
-- @check
SHOW PROCEDURE STATUS WHERE Db = 'belajar_sp';

Output yang Dihasilkan:

text
+------------+---------------+-----------+----------+----------------+---------------------+---------------------+---------------+---------+----------------------+----------------------+--------------------+
| Db         | Name          | Type      | Language | Definer        | Modified            | Created             | Security_type | Comment | character_set_client | collation_connection | Database Collation |
+------------+---------------+-----------+----------+----------------+---------------------+---------------------+---------------+---------+----------------------+----------------------+--------------------+
| belajar_sp | daftar_produk | PROCEDURE | SQL      | root@localhost | 2026-10-14 12:53:39 | 2026-10-14 12:53:39 | DEFINER       |         | utf8mb4              | utf8mb4_0900_ai_ci   | utf8mb4_0900_ai_ci |
| belajar_sp | halo          | PROCEDURE | SQL      | root@localhost | 2026-10-14 12:53:38 | 2026-10-14 12:53:38 | DEFINER       |         | utf8mb4              | utf8mb4_0900_ai_ci   | utf8mb4_0900_ai_ci |
| belajar_sp | ringkasan     | PROCEDURE | SQL      | root@localhost | 2026-10-14 12:53:40 | 2026-10-14 12:53:40 | DEFINER       |         | utf8mb4              | utf8mb4_0900_ai_ci   | utf8mb4_0900_ai_ci |
+------------+---------------+-----------+----------+----------------+---------------------+---------------------+---------------+---------+----------------------+----------------------+--------------------+
3 rows in set (0.00 sec)

Kolom Created dan Modified berisi waktu pembuatan prosedur, sehingga nilainya berbeda-beda di setiap lingkungan dan setiap eksekusi ulang — contoh di atas memperlihatkan salah satu hasil nyata. Kedua, SHOW CREATE PROCEDURE menampilkan definisi utuh prosedur — sangat berguna untuk membandingkan versi yang tersimpan di server dengan berkas sumber:

sql
SHOW CREATE PROCEDURE daftar_produk\G

Output yang Dihasilkan:

text
*************************** 1. row ***************************
           Procedure: daftar_produk
            sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
    Create Procedure: CREATE DEFINER=`root`@`localhost` PROCEDURE `daftar_produk`()
BEGIN
  SELECT id, nama, harga, stok FROM produk ORDER BY id;
END
character_set_client: utf8mb4
collation_connection: utf8mb4_0900_ai_ci
  Database Collation: utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

Ketiga, katalog information_schema.ROUTINES memuat prosedur dan function dalam bentuk tabel standar sehingga bisa ikut difilter dan diurutkan:

sql
SELECT ROUTINE_NAME, ROUTINE_TYPE, DTD_IDENTIFIER
FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = 'belajar_sp'
ORDER BY ROUTINE_NAME;

Output yang Dihasilkan:

text
+---------------+--------------+----------------+
| ROUTINE_NAME  | ROUTINE_TYPE | DTD_IDENTIFIER |
+---------------+--------------+----------------+
| daftar_produk | PROCEDURE    | NULL           |
| halo          | PROCEDURE    | NULL           |
| ringkasan     | PROCEDURE    | NULL           |
+---------------+--------------+----------------+
3 rows in set (0.00 sec)

Ketiga cara menampilkan objek yang sama dari sudut berbeda: status ringkas, definisi lengkap, dan katalog yang bisa diquery seperti tabel biasa.

Membersihkan Database Latihan #

Latihan selesai. Prosedur kita jatuhkan secara eksplisit, lalu database latihan dibuang — prosedur yang tersisa di dalam database ikut lenyap bersama database-nya:

sql
DROP PROCEDURE halo;
DROP PROCEDURE daftar_produk;
DROP PROCEDURE ringkasan;
DROP DATABASE belajar_sp;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Referensi #

Kita kini bisa menyimpan program di dalam server: membuat prosedur dengan pola DELIMITER $$ ... END$$ DELIMITER ;, memanggilnya dengan CALL, menjaga agar pembuatannya idempoten, dan memeriksa prosedur yang tersimpan. Namun prosedur halo, daftar_produk, dan ringkasan masih bekerja dengan nilai tetap — hasilnya sama setiap kali dipanggil. Bab berikutnya menghadirkan parameter: pintu masuk dan pintu keluar data yang membuat prosedur bisa menerima masukan dari pemanggil dan mengembalikan hasil yang berubah sesuai kebutuhan.