Beberapa pertanyaan membutuhkan hasil query lain sebagai bahan: “produk mana yang harganya di atas rata-rata?” menuntut perhitungan rata-rata lebih dulu, baru perbandingan tiap produk terhadap angka tersebut. Subquery — query di dalam query lain — menjawab pola berlapis tersebut dengan rapi. Chapter ini memakai database toko online dari chapter Pengenalan SQL dan ditutup panduan memilih antara subquery dan JOIN.

Subquery Skalar #

Subquery skalar mengembalikan tepat satu baris satu kolom sehingga hasilnya diperlakukan seperti satu nilai biasa. Query dalam kurung dijalankan lebih dahulu, lalu nilainya menggantikan posisi subquery:

sql
SELECT AVG(harga) FROM produk

Output yang Dihasilkan:

text
(3046666.6666666665,)

Angka tersebut kini dipakai sebagai pembanding langsung di klausa WHERE:

sql
SELECT nama, harga
FROM produk
WHERE harga > (SELECT AVG(harga) FROM produk)

Output yang Dihasilkan:

text
('Smartphone X1', 3500000)
('Laptop Pro 14', 12500000)

Mari kita telaah: subquery (SELECT AVG(harga) FROM produk) menghasilkan 3.046.666,67; kondisi harga > 3046666.67 lalu menyaring produk satu per satu dan menyisakan dua produk termahal. Tanpa subquery, kita menjalankan dua query terpisah lalu menyalin angkanya secara manual — lambat dan rawan kesalahan saat data berubah.

Subquery pada WHERE dengan IN dan NOT IN #

Subquery juga boleh mengembalikan satu kolom banyak baris, lalu diuji dengan IN. Query berikut mencari pelanggan yang pernah menyelesaikan pesanan:

sql
SELECT nama
FROM pelanggan
WHERE id IN (SELECT pelanggan_id FROM pesanan WHERE status = 'selesai')

Output yang Dihasilkan:

text
('Budi Santoso',)
('Siti Rahayu',)
('Dewi Lestari',)
('Andi Wijaya',)

Kebalikannya, NOT IN menemukan pelanggan yang belum pernah menyelesaikan pesanan apa pun:

sql
SELECT nama
FROM pelanggan
WHERE id NOT IN (SELECT pelanggan_id FROM pesanan WHERE status = 'selesai')

Output yang Dihasilkan:

text
('Rina Kartika',)
('Farhan Pratama',)

Subquery pada FROM sebagai Derived Table #

Subquery pada klausa FROM — disebut derived table — menghasilkan tabel sementara yang bisa diperlakukan seperti tabel biasa. Pola ini memungkinkan kita memfilter hasil agregasi tanpa HAVING:

sql
SELECT pesanan_id, total
FROM (SELECT pesanan_id, SUM(subtotal) AS total
      FROM detail_pesanan
      GROUP BY pesanan_id) AS ringkasan
WHERE total > 1000000
ORDER BY total DESC

Output yang Dihasilkan:

text
(3, 12500000)
(1, 4000000)
(6, 1500000)
(4, 1150000)
(2, 1130000)

Subquery di dalam kurung merangkum total belanja per pesanan menjadi tabel bernama ringkasan; query luar tinggal menyaring kolom total dengan WHERE biasa. Setiap derived table wajib diberi alias — tanpa AS ringkasan, database menolak query. Perbedaannya dengan HAVING: keduanya sah, tetapi derived table membuat hasil agregasi bisa langsung dipakai ulang oleh query luar.

Correlated Subquery #

Subquery yang sudah kita lihat berdiri sendiri dan bisa dijalankan terpisah. Subquery berkorelasi (correlated subquery) merujuk kolom dari query luar sehingga dievaluasi ulang untuk tiap baris kandidat — contohnya produk yang harganya di atas rata-rata kategorinya sendiri:

sql
SELECT nama, kategori, harga
FROM produk p
WHERE harga > (SELECT AVG(harga) FROM produk WHERE kategori = p.kategori)

Output yang Dihasilkan:

text
('Laptop Pro 14', 'elektronik', 12500000)
('Sneakers Klasik', 'fashion', 900000)

Alias p dari query luar dipanggil di dalam subquery melalui kategori = p.kategori: untuk setiap produk, subquery menghitung rata-rata harga kategorinya (elektronik 5.583.333; fashion 510.000), lalu harga produk dibandingkan dengan angka tersebut. Hanya Laptop Pro 14 dan Sneakers Klasik yang menembus rata-rata kelompok masing-masing.

Menguji Keberadaan dengan EXISTS #

Operator EXISTS menguji apakah subquery mengembalikan setidaknya satu baris — database berhenti memeriksa begitu kecocokan pertama ditemukan, sehingga pola ini efisien untuk pertanyaan keberadaan:

sql
SELECT nama
FROM pelanggan p
WHERE EXISTS (SELECT 1
              FROM pesanan ps
              WHERE ps.pelanggan_id = p.id
                AND ps.status = 'selesai')

Output yang Dihasilkan:

text
('Budi Santoso',)
('Siti Rahayu',)
('Dewi Lestari',)
('Andi Wijaya',)

Subquery berkorelasi pada EXISTS memeriksa pesanan selesai untuk tiap pelanggan; SELECT 1 adalah konvensi bahwa isi baris tidak penting — yang diuji hanya ada atau tidaknya baris. Hasilnya sama dengan query IN sebelumnya; optimizer modern kerap menjalankan keduanya dengan rencana eksekusi serupa. EXISTS lebih cocok saat kondisi pencocokan kompleks atau saat butuh negasi yang aman terhadap NULL (NOT EXISTS).

Subquery atau JOIN #

AspekSubqueryJOIN
Bentuk paling alami untukkeanggotaan, keberadaan, pembanding agregatmenggabungkan kolom dari beberapa tabel
Keterbacaanmenyatakan “apa yang dicari” berlapismenyatakan relasi antar tabel
Kinerjaoptimizer modern kerap menyamakancenderung mudah dioptimasi
RisikoNOT IN dengan NULLfan-out duplikasi baris

Panduan praktisnya: butuh kolom dari tabel lain, pakai JOIN; butuh jawaban sebagai kondisi — daftar, keberadaan, atau angka pembanding — subquery biasanya lebih jelas dibaca. Kedua bentuk boleh dikombinasikan, dan keraguan kinerja dijawab dengan EXPLAIN, bukan tebakan. Rujukan resmi: PostgreSQL dan SQLite .

Query berlapis yang panjang cepat membuat sulit membaca alur logika. Chapter Common Table Expressions memperkenalkan klausa WITH untuk memberi nama pada subquery sehingga query analitik yang kompleks tetap rapi.