Chapter sebelumnya memperlihatkan subquery yang menghasilkan satu nilai dan langsung dipakai sebagai pembanding di WHERE. Bentuk subquery semacam itu punya nama resmi: scalar subquery — subquery yang mengembalikan tepat satu baris dengan satu kolom. Ketentuannya ketat, tetapi imbalannya besar: di mana pun sebuah nilai biasa boleh ditulis — kolom hasil, operand perkalian, pembanding di WHERE — di situ pula sebuah scalar subquery boleh diletakkan. Pada chapter ini kita berlatih memakai scalar subquery di berbagai posisi, membuktikan bahwa subquery kosong menghasilkan NULL tanpa protes, lalu memicu error 1242 ketika subquery mengembalikan lebih dari satu baris.

Menyiapkan Tabel Latihan #

Kita pakai kembali tabel produk dari database belajar_sub dengan data yang sama seperti chapter sebelumnya, sehingga seluruh angka yang kita hitung tetap mudah diverifikasi secara manual:

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

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 AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(50) NOT NULL,
  kategori VARCHAR(30) NOT NULL,
  harga DECIMAL(10,2) NOT NULL
);

INSERT INTO produk (nama, kategori, harga) VALUES
  ('Kopi Gayo', 'kopi', 55000.00),
  ('Kopi Toraja', 'kopi', 62000.00),
  ('Teh Melati', 'teh', 25000.00),
  ('Teh Hijau', 'teh', 30000.00),
  ('Gula Aren', 'pemanis', 15000.00),
  ('Susu Kental', 'pemanis', 18000.00);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

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

Enam produk dengan tiga kategori ini cukup untuk seluruh skenario pada chapter ini. Rata-rata harga keenam produk tetap 34166.666667 dan harga termahal tetap 62000.

Satu Nilai untuk Satu Slot #

Bentuk paling polos dari scalar subquery adalah mengisinya sebagai satu kolom hasil. Query luar bahkan tidak perlu membaca tabel apa pun:

sql
SELECT (SELECT AVG(harga) FROM produk) AS rata_harga;

Output yang Dihasilkan:

text
+--------------+
| rata_harga   |
+--------------+
| 34166.666667 |
+--------------+
1 row in set (0.00 sec)

Subquery di dalam kurung berjalan lebih dulu, menghasilkan satu nilai 34166.666667, lalu nilai tersebut mengisi satu slot kolom bernama rata_harga pada hasil akhir. Bayangkan scalar subquery sebagai kotak yang bentuknya persis seperti angka 34166.67: bisa ditaruh di daftar SELECT, bisa dikalikan, bisa dibandingkan. Satu-satunya syaratnya — kotak itu harus benar-benar berisi tepat satu nilai.

Scalar Subquery dalam Ekspresi Aritmetika #

Karena scalar subquery berperilaku seperti nilai biasa, ia bisa ikut berhitung. Query berikut menghitung selisih harga setiap produk terhadap rata-rata pasar:

sql
SELECT nama, harga, ROUND(harga - (SELECT AVG(harga) FROM produk), 2) AS selisih_dari_rata FROM produk ORDER BY selisih_dari_rata DESC;

Output yang Dihasilkan:

text
+-------------+----------+-------------------+
| nama        | harga    | selisih_dari_rata |
+-------------+----------+-------------------+
| Kopi Toraja | 62000.00 |          27833.33 |
| Kopi Gayo   | 55000.00 |          20833.33 |
| Teh Hijau   | 30000.00 |          -4166.67 |
| Teh Melati  | 25000.00 |          -9166.67 |
| Susu Kental | 18000.00 |         -16166.67 |
| Gula Aren   | 15000.00 |         -19166.67 |
+-------------+----------+-------------------+
6 rows in set (0.00 sec)

Mari kita bedah komposisinya:

  1. (SELECT AVG(harga) FROM produk) — scalar subquery dievaluasi sekali di awal dan menghasilkan 34166.666667.
  2. harga - (...) — setiap baris produk mengurangkan harganya dengan nilai hasil subquery; Kopi Toraja menghasilkan 62000 dikurangi 34166.666667, dibulatkan ROUND menjadi 27833.33.
  3. ORDER BY selisih_dari_rata DESC — alias hasil ekspresi boleh dipakai untuk pengurutan, sehingga produk paling di atas rata-rata tampil paling awal.

Empat baris terbawah menampilkan selisih negatif, yang berarti keempat produk itu di bawah rata-rata. Satu scalar subquery yang sama kini melayani keenam baris tanpa kita perlu menulis angka rata-rata satu kali pun.

Perbandingan dengan Nilai dari Subquery #

Posisi klasik scalar subquery tetap di WHERE, sebagai pembanding. Query berikut mencari produk yang harganya persis sama dengan harga tertinggi:

sql
SELECT nama, harga FROM produk WHERE harga = (SELECT MAX(harga) FROM produk);

Output yang Dihasilkan:

text
+-------------+----------+
| nama        | harga    |
+-------------+----------+
| Kopi Toraja | 62000.00 |
+-------------+----------+
1 row in set (0.00 sec)

MAX dipilih karena dijamin menghasilkan tepat satu baris satu kolom — agregat selalu merangkum seluruh baris inputnya menjadi satu hasil. Pola WHERE kolom = (SELECT MAX/MIN/AVG/COUNT ...) adalah cara paling aman memakai scalar subquery sebagai pembanding, dan pola harga > (SELECT AVG(harga) ...) yang kita pakai di chapter sebelumnya mengikuti aturan yang sama.

Subquery Kosong Menghasilkan NULL #

Apa yang terjadi bila scalar subquery tidak menemukan baris sama sekali? Kategori snack tidak ada di tabel kita, jadi subquery berikut mengembalikan nol baris:

sql
SELECT (SELECT harga FROM produk WHERE kategori = 'snack') AS harga_snack;

Output yang Dihasilkan:

text
+-------------+
| harga_snack |
+-------------+
|        NULL |
+-------------+
1 row in set (0.00 sec)

Tidak ada error yang dilempar. MySQL mengisi slot kolom dengan NULL dan tetap mengembalikan satu baris hasil. Perilaku ini penting diingat karena diam-diam merusak: perbandingan seperti harga > (subquery kosong) menjadi harga > NULL, yang hasilnya NULL untuk semua baris — dan baris dengan kondisi NULL tersaring keluar, sehingga query menghasilkan kosong tanpa pesan apa pun.

Lebih dari Satu Baris: ERROR 1242 #

Masalah sebaliknya lebih riuh: subquery yang mengembalikan lebih dari satu baris. Query berikut sengaja membiarkan subquery membaca seluruh kolom id dari enam produk:

sql
-- @error
SELECT (SELECT id FROM produk);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1242 (21000): Subquery returns more than 1 row
Bye

MySQL menolak query dengan error 1242 (21000): Subquery returns more than 1 row — slot scalar hanya menyediakan ruang untuk satu nilai, sedangkan subquery datang membawa enam. Error ini muncul saat query berjalan, bukan saat sintaksnya diperiksa, sehingga query yang sama bisa lolos di data kecil lalu meledak di produksi begitu data berubah.

Solusinya selalu menjamin subquery menghasilkan tepat satu baris. Bungkus dengan fungsi agregat seperti MAX, MIN, AVG, atau COUNT; atau, bila memang hanya satu baris yang relevan, tambahkan kondisi WHERE yang cukup selektif untuk menyisakan satu baris.

Membersihkan Database Latihan #

Rangkaian latihan selesai dan database latihan kita jatuhkan kembali:

sql
DROP DATABASE belajar_sub;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

Referensi #

Scalar subquery mengisi satu slot nilai, tetapi banyak pertanyaan nyata justru butuh sekumpulan nilai — “produk mana yang pernah masuk pesanan?” tidak bisa dijawab satu angka. Chapter berikutnya membawa subquery ke WHERE dalam bentuk daftar: IN, ANY, dan ALL, lengkap dengan jebakan NULL yang membuat NOT IN menghasilkan kosong.