TL;DR
Subquery itu query di dalam query. Bisa dipake di WHERE, FROM, atau SELECT. Pake buat filter, bikin tabel sementara, atau ngitung nilai buat perbandingan.
Pernah kepikiran ga, gimana caranya cari produk yang harganya di atas rata-rata? Atau gimana caranya nampilin customer yang belanja paling banyak?
Untuk kasus kayak gitu, kamu butuh yang namanya subquery. Subquery itu basically query di dalam query. Kamu nulis satu query, terus hasilnya dipake sama query luar.
Contohnya kayak gini: kamu mau beli baju yang harganya di bawah rata-rata. Pertama, kamu hitung dulu rata-rata harga semua baju. Baru setelah itu, kamu filter baju yang harganya di bawah angka itu. Hitungan rata-rata itu yang jadi subquery-nya.
Sebagai Data Analyst, kamu bakal sering nemuin case yang ga bisa diselesaiin cuma pake WHERE biasa. Contohnya:
Semua itu butuh subquery. Tanpa subquery, query kamu bakal jadi ribet banget atau bahkan ga mungkin dibikin.
Buat tutorial ini, kita pakai dataset e-commerce lokal. Bayangin kamu jadi Data Analyst di startup marketplace Indonesia.
| id | nama_produk | kategori | harga | stok |
|---|---|---|---|---|
| 1 | Batik Pekalongan | Fashion | 185000 | 50 |
| 2 | Kopi Toraja 250g | Makanan | 75000 | 100 |
| 3 | Tas Kulit Garut | Fashion | 450000 | 25 |
| 4 | Sambal Bu Rudy | Makanan | 35000 | 200 |
| 5 | Gelang Perak Bali | Aksesoris | 250000 | 30 |
| 6 | Keripik Tempe | Makanan | 25000 | 150 |
| 7 | Kaos Dagadu | Fashion | 95000 | 80 |
| 8 | Dodol Betawi | Makanan | 45000 | 120 |
| 9 | Cincin Perak | Aksesoris | 175000 | 40 |
| 10 | Mukena Bali | Fashion | 320000 | 35 |
| id | tanggal | customer_id | produk_id | qty | total |
|---|---|---|---|---|---|
| 1 | 2024-01-15 | 101 | 1 | 2 | 370000 |
| 2 | 2024-01-15 | 102 | 2 | 3 | 225000 |
| 3 | 2024-01-16 | 103 | 4 | 5 | 175000 |
| 4 | 2024-01-16 | 101 | 3 | 1 | 450000 |
| 5 | 2024-01-17 | 104 | 6 | 10 | 250000 |
| 6 | 2024-01-17 | 102 | 5 | 1 | 250000 |
| 7 | 2024-01-18 | 105 | 7 | 2 | 190000 |
| 8 | 2024-01-18 | 103 | 8 | 4 | 180000 |
| 9 | 2024-01-19 | 101 | 2 | 2 | 150000 |
| 10 | 2024-01-19 | 106 | 1 | 1 | 185000 |
| id | nama | kota | tgl_daftar |
|---|---|---|---|
| 101 | Budi Santoso | Jakarta | 2023-06-15 |
| 102 | Siti Rahayu | Bandung | 2023-08-20 |
| 103 | Andi Wijaya | Surabaya | 2023-09-10 |
| 104 | Dewi Lestari | Yogyakarta | 2023-11-05 |
| 105 | Reza Pratama | Semarang | 2024-01-02 |
| 106 | Maya Putri | Jakarta | 2024-01-10 |
Ada 3 tempat utama di mana kamu bisa pake subquery:
Kita bahas satu-satu dengan contoh praktis ya.
Ini jenis subquery yang paling sering dipake. Kamu pake hasil dari subquery buat filter data di query utama.
Cari produk yang harganya di atas rata-rata semua produk.
SELECT nama_produk, kategori, harga
FROM produk
WHERE harga > (
SELECT AVG(harga) FROM produk
);
Cara kerjanya:
1. Subquery SELECT AVG(harga) FROM produk dijalankan dulu, hasilnya 165500
2. Query utama filter produk yang harganya > 165500
Hasil:
| nama_produk | kategori | harga |
|---|---|---|
| Batik Pekalongan | Fashion | 185000 |
| Tas Kulit Garut | Fashion | 450000 |
| Gelang Perak Bali | Aksesoris | 250000 |
| Cincin Perak | Aksesoris | 175000 |
| Mukena Bali | Fashion | 320000 |
Cari customer yang total belanjanya sama dengan nilai tertinggi.
SELECT c.nama, c.kota, SUM(t.total) AS total_belanja
FROM customer c
JOIN transaksi t ON c.id = t.customer_id
GROUP BY c.id, c.nama, c.kota
HAVING SUM(t.total) = (
SELECT MAX(total_per_customer)
FROM (
SELECT customer_id, SUM(total) AS total_per_customer
FROM transaksi
GROUP BY customer_id
) AS sub
);
Hasil:
| nama | kota | total_belanja |
|---|---|---|
| Budi Santoso | Jakarta | 970000 |
Di sini kita pake nested subquery. Subquery dalem ngitung total belanja per customer, subquery luar ambil nilai MAX-nya.
Cari transaksi yang produknya termasuk kategori Fashion.
SELECT t.id, t.tanggal, p.nama_produk, t.total
FROM transaksi t
JOIN produk p ON t.produk_id = p.id
WHERE t.produk_id IN (
SELECT id FROM produk WHERE kategori = 'Fashion'
);
Hasil:
| id | tanggal | nama_produk | total |
|---|---|---|---|
| 1 | 2024-01-15 | Batik Pekalongan | 370000 |
| 4 | 2024-01-16 | Tas Kulit Garut | 450000 |
| 7 | 2024-01-18 | Kaos Dagadu | 190000 |
| 10 | 2024-01-19 | Batik Pekalongan | 185000 |
Cari produk yang BELUM pernah terjual.
SELECT id, nama_produk, kategori
FROM produk
WHERE id NOT IN (
SELECT DISTINCT produk_id FROM transaksi
);
Hasil:
| id | nama_produk | kategori |
|---|---|---|
| 9 | Cincin Perak | Aksesoris |
| 10 | Mukena Bali | Fashion |
Ini berguna banget buat analisis produk yang ga laku.
Cari produk yang harganya lebih mahal dari SEMUA produk kategori Makanan.
SELECT nama_produk, kategori, harga
FROM produk
WHERE harga > ALL (
SELECT harga FROM produk WHERE kategori = 'Makanan'
);
Hasil:
| nama_produk | kategori | harga |
|---|---|---|
| Batik Pekalongan | Fashion | 185000 |
| Tas Kulit Garut | Fashion | 450000 |
| Gelang Perak Bali | Aksesoris | 250000 |
| Kaos Dagadu | Fashion | 95000 |
| Cincin Perak | Aksesoris | 175000 |
| Mukena Bali | Fashion | 320000 |
Harga tertinggi di kategori Makanan itu 75000 (Kopi Toraja). Jadi semua produk di atas 75000 masuk ke hasil.
Subquery di FROM bikin tabel sementara yang bisa kamu query lagi. Ini berguna banget buat analisis yang kompleks.
SELECT
ranking.nama,
ranking.kota,
ranking.total_belanja,
ranking.jumlah_transaksi
FROM (
SELECT
c.nama,
c.kota,
SUM(t.total) AS total_belanja,
COUNT(*) AS jumlah_transaksi
FROM customer c
JOIN transaksi t ON c.id = t.customer_id
GROUP BY c.id, c.nama, c.kota
) AS ranking
ORDER BY ranking.total_belanja DESC;
Hasil:
| nama | kota | total_belanja | jumlah_transaksi |
|---|---|---|---|
| Budi Santoso | Jakarta | 970000 | 3 |
| Siti Rahayu | Bandung | 475000 | 2 |
| Andi Wijaya | Surabaya | 355000 | 2 |
| Dewi Lestari | Yogyakarta | 250000 | 1 |
| Reza Pratama | Semarang | 190000 | 1 |
| Maya Putri | Jakarta | 185000 | 1 |
Hitung persentase kontribusi tiap kategori terhadap total penjualan.
SELECT
kategori_stats.kategori,
kategori_stats.total_penjualan,
total_stats.grand_total,
ROUND(
kategori_stats.total_penjualan * 100.0 / total_stats.grand_total,
2
) AS persentase
FROM (
SELECT
p.kategori,
SUM(t.total) AS total_penjualan
FROM produk p
JOIN transaksi t ON p.id = t.produk_id
GROUP BY p.kategori
) AS kategori_stats
CROSS JOIN (
SELECT SUM(total) AS grand_total FROM transaksi
) AS total_stats
ORDER BY persentase DESC;
Hasil:
| kategori | total_penjualan | grand_total | persentase |
|---|---|---|---|
| Fashion | 1195000 | 2425000 | 49.28 |
| Makanan | 980000 | 2425000 | 40.41 |
| Aksesoris | 250000 | 2425000 | 10.31 |
Nah, ini insight yang valuable banget. Fashion nyumbang hampir 50% total penjualan!
Subquery di SELECT dipake buat ngitung nilai yang bakal muncul sebagai kolom baru di tiap baris.
Tampilin harga produk beserta rata-rata harga kategorinya.
SELECT
nama_produk,
kategori,
harga,
(
SELECT ROUND(AVG(harga), 0)
FROM produk p2
WHERE p2.kategori = p1.kategori
) AS avg_kategori,
harga - (
SELECT ROUND(AVG(harga), 0)
FROM produk p2
WHERE p2.kategori = p1.kategori
) AS selisih_dari_avg
FROM produk p1
ORDER BY kategori, harga DESC;
Hasil:
| nama_produk | kategori | harga | avg_kategori | selisih_dari_avg |
|---|---|---|---|---|
| Gelang Perak Bali | Aksesoris | 250000 | 212500 | 37500 |
| Cincin Perak | Aksesoris | 175000 | 212500 | -37500 |
| Tas Kulit Garut | Fashion | 450000 | 262500 | 187500 |
| Mukena Bali | Fashion | 320000 | 262500 | 57500 |
| Batik Pekalongan | Fashion | 185000 | 262500 | -77500 |
| Kaos Dagadu | Fashion | 95000 | 262500 | -167500 |
| Kopi Toraja 250g | Makanan | 75000 | 45000 | 30000 |
| Dodol Betawi | Makanan | 45000 | 45000 | 0 |
| Sambal Bu Rudy | Makanan | 35000 | 45000 | -10000 |
| Keripik Tempe | Makanan | 25000 | 45000 | -20000 |
Ini berguna buat liat mana produk yang overpriced atau underpriced dibanding rata-rata kategorinya.
Tampilin produk beserta jumlah transaksinya.
SELECT
p.nama_produk,
p.kategori,
p.harga,
(
SELECT COUNT(*)
FROM transaksi t
WHERE t.produk_id = p.id
) AS jumlah_terjual
FROM produk p
ORDER BY jumlah_terjual DESC;
Hasil:
| nama_produk | kategori | harga | jumlah_terjual |
|---|---|---|---|
| Batik Pekalongan | Fashion | 185000 | 2 |
| Kopi Toraja 250g | Makanan | 75000 | 2 |
| Tas Kulit Garut | Fashion | 450000 | 1 |
| Sambal Bu Rudy | Makanan | 35000 | 1 |
| Gelang Perak Bali | Aksesoris | 250000 | 1 |
| Keripik Tempe | Makanan | 25000 | 1 |
| Kaos Dagadu | Fashion | 95000 | 1 |
| Dodol Betawi | Makanan | 45000 | 1 |
| Cincin Perak | Aksesoris | 175000 | 0 |
| Mukena Bali | Fashion | 320000 | 0 |
Correlated subquery itu subquery yang reference kolom dari query luar. Setiap baris di query utama, subquery-nya dijalankan ulang.
SELECT
t.id,
c.nama,
t.tanggal,
t.total
FROM transaksi t
JOIN customer c ON t.customer_id = c.id
WHERE t.total = (
SELECT MAX(total)
FROM transaksi t2
WHERE t2.customer_id = t.customer_id
);
Hasil:
| id | nama | tanggal | total |
|---|---|---|---|
| 4 | Budi Santoso | 2024-01-16 | 450000 |
| 6 | Siti Rahayu | 2024-01-17 | 250000 |
| 5 | Dewi Lestari | 2024-01-17 | 250000 |
| 7 | Reza Pratama | 2024-01-18 | 190000 |
| 10 | Maya Putri | 2024-01-19 | 185000 |
| 8 | Andi Wijaya | 2024-01-18 | 180000 |
Di sini, t.customer_id di subquery nge-refer ke baris yang lagi diproses di query utama.
Kadang kamu bisa pake IN atau EXISTS buat hasil yang sama. Tapi ada perbedaan performa.
SELECT nama_produk
FROM produk
WHERE id IN (SELECT produk_id FROM transaksi);
SELECT nama_produk
FROM produk p
WHERE EXISTS (
SELECT 1 FROM transaksi t WHERE t.produk_id = p.id
);
Kapan pake apa?
- EXISTS lebih cepat kalau subquery hasilnya banyak
- IN lebih cepat kalau subquery hasilnya sedikit
- EXISTS lebih aman untuk handle NULL
- IN lebih readable untuk case simple
-- SALAH kalau subquery return > 1 row
SELECT * FROM produk
WHERE harga = (SELECT harga FROM produk WHERE kategori = 'Fashion');
Fix: Pake IN atau agregasi function.
-- BENAR
SELECT * FROM produk
WHERE harga IN (SELECT harga FROM produk WHERE kategori = 'Fashion');
Kadang subquery bisa di-replace dengan JOIN yang lebih efficient.
-- Bisa jadi lambat
SELECT nama_produk
FROM produk
WHERE id IN (
SELECT produk_id FROM transaksi WHERE total > 200000
);
-- Lebih efficient pake JOIN
SELECT DISTINCT p.nama_produk
FROM produk p
JOIN transaksi t ON p.id = t.produk_id
WHERE t.total > 200000;
-- SALAH
SELECT * FROM (
SELECT kategori, SUM(total) FROM transaksi GROUP BY kategori
);
-- BENAR
SELECT * FROM (
SELECT kategori, SUM(total) AS total
FROM transaksi
GROUP BY kategori
) AS sub; -- Wajib pake alias
Buat query yang kompleks, kadang lebih baik pake CTE daripada nested subquery.
-- Pake Subquery (susah dibaca)
SELECT *
FROM (
SELECT kategori, SUM(total) AS total
FROM (
SELECT p.kategori, t.total
FROM produk p
JOIN transaksi t ON p.id = t.produk_id
) AS sub1
GROUP BY kategori
) AS sub2
WHERE total > 300000;
-- Pake CTE (lebih readable)
WITH sales_data AS (
SELECT p.kategori, t.total
FROM produk p
JOIN transaksi t ON p.id = t.produk_id
),
kategori_total AS (
SELECT kategori, SUM(total) AS total
FROM sales_data
GROUP BY kategori
)
SELECT * FROM kategori_total WHERE total > 300000;
CTE bikin query lebih modular dan gampang di-debug.
Sebelum jalanin query lengkap, test subquery-nya terpisah. Pastiin hasilnya sesuai ekspektasi.
Subquery di SELECT atau correlated subquery bisa lambat karena dijalankan per baris. Kalau datanya banyak, consider pake JOIN atau CTE.
Kasih alias yang meaningful biar query gampang dibaca.
Kalau subquery-nya nested, tambahin comment biar teammate (atau kamu di masa depan) bisa ngerti.
Subquery itu tool yang berguna banget buat Data Analyst. Inget poin-poin utama:
Mulai dari contoh simple, terus gradually naikin kompleksitasnya. Latihan bikin subquery sendiri pake dataset yang kamu punya.
Cek nguliksql.id buat latihan lebih banyak.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Cara pakai DATE_TRUNC di SQL buat memotong timestamp jadi awal bulan, minggu, atau jam. Kunci bikin laporan penjualan per periode yang rapi.
Cara mengisi tanggal kosong di time series SQL pakai generate_series dan LEFT JOIN biar grafik dan laporan gak bolong. Contoh penjualan harian UMKM.
Analisa per kuartal, hari kerja, atau musim jadi ribet kalau tiap query ngitung ulang atribut tanggal. Tabel kalender nyimpen semua atribut itu sekali, biar tinggal di-JOIN.