Sejauh ini setiap query yang kita tulis berdiri sendiri: satu pernyataan SELECT membaca satu atau beberapa tabel, lalu selesai. Pertanyaan bisnis nyata sering tidak semudah itu. Pertanyaan “produk mana yang harganya di atas rata-rata?” tidak bisa dijawab satu query datar, karena nilai rata-ratanya sendiri baru diketahui setelah query dijalankan — dan angka itu berubah setiap kali data berubah. Subquery menjawab kebutuhan ini: sebuah query lengkap yang diletakkan di dalam query lain, selalu dibungkus tanda kurung, dan hasilnya dipakai oleh query di sekelilingnya. Pada chapter ini kita membedah contoh subquery pertama selangkah demi selangkah, memahami urutan evaluasinya, lalu memetakan tiga posisi utama tempat subquery bisa ditempatkan.
Menyiapkan Tabel Latihan #
Kita siapkan database latihan belajar_sub yang akan dipakai konsisten di seluruh section ini. Tabel pertama adalah produk milik toko daring kecil yang menjual kopi, teh, dan pemanis dengan enam baris data yang nilainya sengaja mudah dihitung ulang secara manual:
DROP DATABASE IF EXISTS belajar_sub;
CREATE DATABASE belajar_sub;
USE belajar_sub;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 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:
Query OK, 0 rows affected (0.00 sec)
Query OK, 6 rows affected (0.00 sec)
Records: 6 Duplicates: 0 Warnings: 0Total keenam harga adalah 205000, sehingga rata-ratanya 205000 dibagi 6 — angka pecahan yang tidak enak disalin manual. Data ini akan menjadi bahan utama seluruh contoh pada chapter ini.
Pertanyaan yang Tidak Bisa Dijawab Satu Langkah #
Kita mulai dari cara naif yang sering dipakai pemula: menjalankan query pertama untuk membaca rata-rata, lalu menyalin hasilnya menjadi angka tetap di query kedua. Langkah pertama menghitung rata-rata harga:
SELECT AVG(harga) AS rata_harga FROM produk;Output yang Dihasilkan:
+--------------+
| rata_harga |
+--------------+
| 34166.666667 |
+--------------+
1 row in set (0.00 sec)Rata-rata harga produk adalah 34166.666667. Langkah kedua memakai angka itu sebagai ambang penyaringan yang ditulis manual:
SELECT nama, harga FROM produk WHERE harga > 34166.67 ORDER BY harga DESC;Output yang Dihasilkan:
+-------------+----------+
| nama | harga |
+-------------+----------+
| Kopi Toraja | 62000.00 |
| Kopi Gayo | 55000.00 |
+-------------+----------+
2 rows in set (0.00 sec)Dua produk lolos: Kopi Toraja dan Kopi Gayo. Pendekatan dua langkah ini memang menghasilkan jawaban, tetapi rapuh. Begitu satu produk baru masuk atau satu harga berubah, angka 34166.67 yang tertulis di query menjadi basi, dan setiap orang harus mengulang ritual “hitung dulu, salin belakangan” — proses yang rawan salah ketik dan tidak bisa disimpan sebagai satu laporan siap pakai.
Subquery: Query di Dalam Query #
Subquery merapikan ritual dua langkah itu menjadi satu pernyataan. Query yang menghitung rata-rata kita selipkan langsung ke dalam kondisi WHERE:
SELECT nama, harga FROM produk WHERE harga > (SELECT AVG(harga) FROM produk) ORDER BY harga DESC;Output yang Dihasilkan:
+-------------+----------+
| nama | harga |
+-------------+----------+
| Kopi Toraja | 62000.00 |
| Kopi Gayo | 55000.00 |
+-------------+----------+
2 rows in set (0.00 sec)Hasilnya identik dengan versi dua langkah: dua produk yang sama, urutan yang sama. Mari kita bedah perannya bagian per bagian:
- Bagian dalam tanda kurung —
(SELECT AVG(harga) FROM produk)adalah subquery: query lengkap denganSELECT, klausa, dan tabelnya sendiri, yang ditempatkan sebagai operan dari pembanding>. - Bagian di luar tanda kurung —
SELECT nama, harga FROM produk WHERE ...adalah outer query (query luar) yang memperlakukan hasil subquery seolah-olah sebuah nilai biasa di sisi kanan pembanding. - Tanda kurung — tanda kurung bukan gaya penulisan opsional; subquery wajib dibungkus kurung agar MySQL tahu di mana query bagian dalam mulai dan berakhir.
ORDER BY harga DESC— pengurutan menurun tetap berlaku pada hasil akhir seperti biasa, tidak peduli ada subquery di dalamnya.
Ketika harga berubah atau produk baru ditambahkan, query yang sama tetap benar, karena rata-rata selalu dihitung ulang dari data terkini pada setiap eksekusi.
Subquery Dievaluasi Lebih Dulu #
Logika eksekusinya mudah diingat: bagian dalam berjalan sebelum bagian luar memakai hasilnya. MySQL menjalankan SELECT AVG(harga) FROM produk dan mendapatkan satu nilai, yaitu 34166.666667; setelah nilai itu tersedia, outer query berjalan seolah-olah ditulis WHERE harga > 34166.666667. Subquery bekerja seperti fungsi yang mengembalikan nilai — kita tidak perlu tahu angkanya saat menulis query, cukup tahu bahwa satu nilai pasti tersedia saat query berjalan.
Tiga Posisi Subquery dalam Satu Query #
Subquery tidak hanya hidup di WHERE. Tiga posisi utamanya di MySQL dapat dipetakan sebagai berikut:
| Posisi | Bentuk ringkas | Kegunaan |
|---|---|---|
WHERE | WHERE kolom > (subquery) | Menyaring baris memakai nilai, daftar nilai, atau kondisi keberadaan hasil subquery |
SELECT | SELECT kolom, (subquery) AS alias | Menambah kolom baru yang nilainya dihitung dari subquery |
FROM | FROM (subquery) AS alias | Memakai hasil subquery seolah-olah sebuah tabel |
Dua contoh berikut adalah cuplikan kecil dari masing-masing posisi, memakai tabel produk yang sudah kita siapkan. Posisi SELECT menambahkan satu kolom yang isinya sama untuk semua baris:
SELECT p.nama, (SELECT MAX(harga) FROM produk) AS harga_termahal FROM produk p ORDER BY p.id;Output yang Dihasilkan:
+-------------+----------------+
| nama | harga_termahal |
+-------------+----------------+
| Kopi Gayo | 62000.00 |
| Kopi Toraja | 62000.00 |
| Teh Melati | 62000.00 |
| Teh Hijau | 62000.00 |
| Gula Aren | 62000.00 |
| Susu Kental | 62000.00 |
+-------------+----------------+
6 rows in set (0.00 sec)Setiap baris menampilkan nama produk berdampingan dengan kolom harga_termahal yang nilainya selalu 62000 — subquery ini menghitung satu nilai global, sehingga semua baris menerima nilai yang sama. Posisi FROM memakai hasil query sebagai tabel sumber:
SELECT r.kategori, r.rata FROM (SELECT kategori, AVG(harga) AS rata FROM produk GROUP BY kategori) AS r ORDER BY r.kategori;Output yang Dihasilkan:
+----------+--------------+
| kategori | rata |
+----------+--------------+
| kopi | 58500.000000 |
| pemanis | 16500.000000 |
| teh | 27500.000000 |
+----------+--------------+
3 rows in set (0.00 sec)Query di dalam kurung mengelompokkan produk per kategori dan menghitung rata-rata harga; hasil tiga barisnya kemudian diperlakukan sebagai tabel bernama r oleh query luar. Kedua cuplikan ini sengaja hanya disinggung — masing-masing posisi dibedah tuntas pada chapter khususnya di sepanjang section ini.
Membersihkan Database Latihan #
Rangkaian latihan selesai dan database latihan kita jatuhkan kembali:
DROP DATABASE belajar_sub;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)Referensi #
- Subqueries — MySQL 8.4 Reference Manual
- SELECT Statement — MySQL 8.4 Reference Manual
- Aggregate Function Descriptions — MySQL 8.4 Reference Manual
Contoh pertama kita memperlihatkan subquery yang menghasilkan tepat satu nilai dan dipakai seperti nilai biasa di sisi kanan pembanding. Bentuk subquery ini punya nama resmi — scalar subquery — dan aturan mainnya yang ketat layak dibahas tersendiri. Chapter berikutnya membedah scalar subquery: apa yang terjadi bila hasilnya kosong, dan error apa yang muncul bila subquery mengembalikan lebih dari satu baris.