Sebagian pertanyaan tidak bisa dijawab satu SELECT mendatar karena jawabannya sendiri adalah hasil query lain: “produk mana yang harganya di atas rata-rata” mengharuskan rata-rata dihitung lebih dulu. Subquery — query di dalam query — menyelesaikan masalah ini dengan menjalankan SELECT bagian dalam terlebih dahulu, lalu menyerahkan hasilnya ke query luar. Kita akan membedah lima bentuknya satu per satu, ditutup jebakan NOT IN ber-NULL yang sempat kita lihat sekilas pada Chapter Operator Lanjutan: IN, BETWEEN, IS NULL.
Data Contoh #
Empat tabel domain toko dipakai sepanjang chapter ini:
CREATE TABLE pelanggan (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
kota TEXT
);
CREATE TABLE produk (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
harga INTEGER NOT NULL,
stok INTEGER
);
CREATE TABLE pesanan (
id INTEGER PRIMARY KEY,
pelanggan_id INTEGER REFERENCES pelanggan(id),
tanggal TEXT NOT NULL
);
CREATE TABLE detail_pesanan (
pesanan_id INTEGER REFERENCES pesanan(id),
produk_id INTEGER REFERENCES produk(id),
jumlah INTEGER NOT NULL,
PRIMARY KEY (pesanan_id, produk_id)
);
INSERT INTO pelanggan (id, nama, kota) VALUES
(1, 'Budi', 'Jakarta'), (2, 'Siti', 'Bandung'), (3, 'Ani', NULL),
(4, 'Rina', 'Surabaya'), (5, 'Dewi', 'Jakarta');
INSERT INTO produk (id, nama, harga, stok) VALUES
(1, 'Kopi Gayo', 45000, 20), (2, 'Teh Melati', 18000, 35),
(3, 'Gula Aren', 25000, 15), (4, 'Sirup Vanila', 25000, NULL),
(5, 'Kopi Toraja', 52000, 10), (6, 'Teh Hijau', 22000, 0),
(7, 'Cangkir Keramik', 35000, 8);
INSERT INTO pesanan (id, pelanggan_id, tanggal) VALUES
(1, 1, '2025-06-01'), (2, 2, '2025-06-03'), (3, 1, '2025-06-07'),
(4, 3, '2025-06-10'), (5, 2, '2025-06-15'), (6, 1, '2025-07-02');
INSERT INTO detail_pesanan (pesanan_id, produk_id, jumlah) VALUES
(1, 1, 2), (1, 3, 1), (2, 2, 3), (3, 1, 1),
(3, 4, 2), (4, 5, 1), (5, 6, 2), (5, 3, 2), (6, 1, 1);Scalar Subquery: Satu Nilai #
Scalar subquery adalah subquery yang hasilnya tepat satu baris satu kolom, sehingga bisa ditanam di posisi mana pun sebuah nilai berada — misalnya di sisi kanan pembanding. Rata-rata harga dihitung dulu oleh query bagian dalam, lalu dipakai sebagai ambang oleh query luar:
SELECT nama, harga
FROM produk
WHERE harga > (SELECT AVG(harga) FROM produk)
ORDER BY harga;Output yang Dihasilkan:
Cangkir Keramik|35000
Kopi Gayo|45000
Kopi Toraja|52000Rata-rata harga tujuh produk adalah 31714.29 (dibulatkan), jadi tiga produk termahal lolos. Penulisan satu nilai rata-rata itu secara manual memang bisa — tetapi scalar subquery selalu mengikuti data terkini tanpa perlu dihitung ulang manusia.
Subquery di FROM: Butuh Alias #
Subquery bisa juga diperlakukan seperti tabel sementara dengan menaruhnya di klausa FROM. Karena hasilnya perlu punya nama agar bisa dirujuk, beri alias — kebiasaan yang juga dituntut oleh sebagian besar database server lain:
SELECT MAX(total) AS tertinggi
FROM (
SELECT pesanan_id, SUM(jumlah) AS total
FROM detail_pesanan
GROUP BY pesanan_id
) AS ringkasan;Output yang Dihasilkan:
4Subquery bagian dalam merangkum jumlah unit per pesanan menjadi tabel kecil ringkasan; query luar lalu mengambil nilai terbesarnya — pesanan 5 dengan empat unit.
Subquery dengan IN #
Ketika hasil subquery berupa kolom nilai, IN menjadikannya daftar penyaring. Kita sudah melihat sekilas pola ini; inilah bentuk lengkapnya — produk yang pernah masuk keranjang:
SELECT nama
FROM produk
WHERE id IN (SELECT produk_id FROM detail_pesanan)
ORDER BY nama;Output yang Dihasilkan:
Gula Aren
Kopi Gayo
Kopi Toraja
Sirup Vanila
Teh Hijau
Teh MelatiMembaliknya dengan NOT IN menjawab “produk mana yang belum pernah dipesan” — Cangkir Keramik satu-satunya. Tetapi NOT IN menyimpan jebakan besar yang kita bahas setelah ini.
Correlated Subquery: Merujuk Query Luar #
Subquery biasa berjalan sekali lalu selesai. Correlated subquery berbeda: subquery-nya merujuk kolom dari query luar (p.id), sehingga engine SQLite menjalankannya sekali per baris luar. Di bawah ini, agregat SUM dihitung terpisah untuk setiap produk:
SELECT p.nama,
(SELECT SUM(d.jumlah) FROM detail_pesanan d WHERE d.produk_id = p.id) AS total_terjual
FROM produk p
ORDER BY p.id;Output yang Dihasilkan:
Kopi Gayo|4
Teh Melati|3
Gula Aren|3
Sirup Vanila|2
Kopi Toraja|1
Teh Hijau|2
Cangkir Keramik|Cangkir Keramik tampil dengan kolom kosong: subquerynya tidak menemukan baris, dan SUM atas himpunan kosong menghasilkan NULL — perilaku yang sudah kita bahas pada Chapter Fungsi Agregat.
EXISTS dan NOT EXISTS #
Untuk pertanyaan “ada atau tidak”, EXISTS lebih tepat daripada IN. Subquery-nya correlated dan cukup mengembalikan baris apa pun — engine berhenti begitu satu baris ditemukan, tanpa menghitung apa pun:
SELECT nama
FROM pelanggan p
WHERE EXISTS (SELECT 1 FROM pesanan o WHERE o.pelanggan_id = p.id)
ORDER BY nama;Output yang Dihasilkan:
Ani
Budi
SitiSELECT nama
FROM pelanggan p
WHERE NOT EXISTS (SELECT 1 FROM pesanan o WHERE o.pelanggan_id = p.id)
ORDER BY nama;Output yang Dihasilkan:
Dewi
RinaKonvensi SELECT 1 menegaskan bahwa isinya tidak penting; EXISTS hanya menguji keberadaan baris.
NOT IN dan NULL: Jebakan Lanjutan #
Ingat aturan dari Chapter Operator Lanjutan: IN, BETWEEN, IS NULL: satu NULL di dalam daftar membuat NOT IN menghasilkan NULL untuk semua nilai yang tidak cocok. Dengan subquery, NULL itu datang diam-diam dari data. Kolom kota pelanggan Ani adalah NULL, sehingga subquery berikut mengandung NULL dan query luar mengembalikan nol baris:
SELECT nama
FROM pelanggan
WHERE kota NOT IN (SELECT kota FROM pelanggan WHERE nama <> 'Siti');Output yang Dihasilkan:
Saring NULL di dalam subquery, dan hasilnya kembali hadir — kini hanya Siti yang kotanya berbeda dari kota semua pelanggan lain yang tercatat:
SELECT nama
FROM pelanggan
WHERE kota NOT IN (
SELECT kota FROM pelanggan WHERE nama <> 'Siti' AND kota IS NOT NULL
);Output yang Dihasilkan:
SitiKapan subquery, kapan JOIN? Keduanya sering bisa saling menggantikan, dan query planner SQLite kerap menghasilkan rencana eksekusi yang sama. Panduan praktisnya: pakai subquery saat hanya butuh satu nilai atau sekadar menguji keberadaan; pakai JOIN saat hasil akhir membutuhkan kolom dari beberapa tabel sekaligus — seperti laporan gabungan pada Chapter Menggabungkan Tabel dengan JOIN.