Tutorial ini akan mengajakmu belajar subquery SQL secara bertahap, mulai dari konsep paling dasar sampai bisa menulis query bertingkat untuk kasus nyata di pekerjaan. Ikuti urutannya satu per satu — setiap bagian dibangun di atas bagian sebelumnya.
Supaya bisa langsung dipraktikkan, kita akan membuat tabelnya dulu sebelum masuk ke query. Anggap kita punya sebuah tabel bernama karyawan seperti ini:
| id | nama | divisi | gaji |
|---|---|---|---|
| 1 | Andi | IT | 9000000 |
| 2 | Budi | IT | 7000000 |
| 3 | Citra | Finance | 8500000 |
| 4 | Dewi | Finance | 6000000 |
| 5 | Eko | Marketing | 5500000 |
Langkah 0: Siapkan Tabelnya
Sebelum menulis query apa pun, buat dulu tabelnya dan isi datanya. Jalankan ini di database yang kamu pakai (contoh berikut memakai sintaks PostgreSQL/MySQL, sesuaikan tipe data kalau enginemu berbeda):
sql
CREATE TABLE karyawan (
id INT PRIMARY KEY,
nama VARCHAR(50),
divisi VARCHAR(50),
gaji INT
);
INSERT INTO karyawan (id, nama, divisi, gaji) VALUES
(1, 'Andi', 'IT', 9000000),
(2, 'Budi', 'IT', 7000000),
(3, 'Citra', 'Finance', 8500000),
(4, 'Dewi', 'Finance', 6000000),
(5, 'Eko', 'Marketing', 5500000);
Tabel tambahan lain (departemen, pelanggan, transaksi, dan seterusnya) akan dibuat sesuai kebutuhan di langkah tempat mereka pertama kali dipakai, supaya kamu tahu persis kapan dan kenapa tabel itu diperlukan.
Langkah 1: Kenali Masalahnya Dulu
Sebelum belajar sintaksnya, pahami dulu kenapa subquery dibutuhkan.
Coba jawab pertanyaan ini dengan SQL: “Siapa saja karyawan yang gajinya di atas rata-rata seluruh karyawan?”
Kamu tidak bisa langsung menulis:
sql
SELECT nama, gaji
FROM karyawan
WHERE gaji > AVG(gaji); -- ERROR, tidak bisa begini
Ini akan gagal, karena AVG(gaji) adalah hasil agregasi yang butuh dihitung dulu secara terpisah, sebelum dipakai untuk membandingkan tiap baris. Artinya, kita butuh dua langkah:
- Hitung dulu rata-rata gaji semua karyawan → hasilnya satu angka.
- Pakai angka itu untuk menyaring baris karyawan satu per satu.
Nah, cara menggabungkan dua langkah ini dalam satu query itulah yang disebut subquery.
Langkah 2: Pahami Definisi Subquery
Subquery adalah query SQL yang diletakkan di dalam query SQL lainnya — istilahnya “query di dalam query”. Subquery biasanya dijalankan lebih dulu oleh database, lalu hasilnya dipakai oleh query utama (disebut outer query).
Sekarang mari selesaikan pertanyaan di Langkah 1:
sql
SELECT nama, gaji
FROM karyawan
WHERE gaji > (SELECT AVG(gaji) FROM karyawan);
Perhatikan bagian (SELECT AVG(gaji) FROM karyawan) — itulah subquery-nya. Alurnya:
- Database menjalankan subquery dulu → didapat satu angka, misalnya
7200000. - Angka itu “disisipkan” ke outer query, seolah-olah kamu menulis
WHERE gaji > 7200000. - Baru query utama dijalankan untuk menyaring baris.
Cobalah jalankan ini di kepala dengan data tabel di atas. Rata-ratanya adalah (9000000+7000000+8500000+6000000+5500000)/5 = 7200000. Jadi hasilnya adalah Andi dan Citra.
Langkah 3: Kenali Tempat-Tempat Subquery Bisa Diletakkan
Subquery tidak hanya bisa ditaruh di WHERE. Ada empat tempat umum:
- Di
WHERE— untuk menyaring baris (contoh Langkah 2 di atas) - Di
SELECT— untuk menghasilkan kolom tambahan - Di
FROM— dipakai sebagai tabel sementara - Di
HAVING— untuk menyaring hasil agregasi
Kita akan coba satu per satu mulai dari langkah berikutnya, dari yang paling sederhana ke yang paling kompleks.
Langkah 4: Coba Subquery Skalar (Hasil Satu Nilai)
Subquery skalar adalah subquery yang hanya mengembalikan satu baris, satu kolom. Ini jenis paling sederhana, dan bisa dipakai di SELECT untuk menambahkan kolom pembanding:
sql
SELECT nama, gaji,
(SELECT MAX(gaji) FROM karyawan) AS gaji_tertinggi
FROM karyawan;
Query ini menampilkan gaji tiap karyawan, sekaligus menyandingkannya dengan gaji tertinggi di seluruh perusahaan pada setiap baris. Cocok dipakai ketika kamu ingin menambahkan angka pembanding tanpa perlu GROUP BY.
Aturan pentingnya: subquery skalar hanya boleh mengembalikan satu nilai. Kalau subquery-nya mengembalikan lebih dari satu baris, query akan error.
Langkah 5: Naik Level ke Subquery dengan Banyak Baris
Sekarang bayangkan kita punya tabel kedua bernama departemen:
| id | nama_departemen | lokasi |
|---|---|---|
| 1 | IT | Jakarta |
| 2 | Finance | Bandung |
| 3 | Marketing | Jakarta |
Buat dulu tabelnya:
sql
CREATE TABLE departemen (
id INT PRIMARY KEY,
nama_departemen VARCHAR(50),
lokasi VARCHAR(50)
);
INSERT INTO departemen (id, nama_departemen, lokasi) VALUES
(1, 'IT', 'Jakarta'),
(2, 'Finance', 'Bandung'),
(3, 'Marketing', 'Jakarta');
Pertanyaannya: “Karyawan mana saja yang divisinya berlokasi di Jakarta?”
Karena subquery SELECT id FROM departemen WHERE lokasi = 'Jakarta' bisa mengembalikan lebih dari satu baris, kita tidak bisa pakai =. Di sinilah operator IN dipakai:
sql
SELECT nama
FROM karyawan
WHERE divisi IN (
SELECT nama_departemen FROM departemen WHERE lokasi = 'Jakarta'
);
Selain IN, ada juga ANY dan ALL untuk kasus serupa yang melibatkan perbandingan angka, misalnya WHERE gaji > ANY (subquery) yang berarti “lebih besar dari setidaknya salah satu hasil subquery”.
Langkah 6: Pelajari EXISTS untuk Mengecek Keberadaan Data
Kadang kamu tidak perlu tahu nilai dari tabel lain — kamu hanya perlu tahu apakah datanya ada atau tidak ada. Untuk ini, dipakai EXISTS atau NOT EXISTS.
Misalnya ada tabel pelanggan dan transaksi, dan kamu ingin tahu “pelanggan mana yang belum pernah bertransaksi?” Buat dulu tabelnya:
sql
CREATE TABLE pelanggan (
id INT PRIMARY KEY,
nama VARCHAR(50),
email VARCHAR(100)
);
CREATE TABLE transaksi (
id INT PRIMARY KEY,
pelanggan_id INT,
jumlah INT,
FOREIGN KEY (pelanggan_id) REFERENCES pelanggan(id)
);
INSERT INTO pelanggan (id, nama, email) VALUES
(1, 'Fajar', '[email protected]'),
(2, 'Gita', '[email protected]'),
(3, 'Hadi', '[email protected]');
-- Fajar dan Gita pernah bertransaksi, Hadi belum
INSERT INTO transaksi (id, pelanggan_id, jumlah) VALUES
(1, 1, 250000),
(2, 2, 100000);
Sekarang kita bisa cek siapa yang belum pernah bertransaksi:
sql
SELECT nama, email
FROM pelanggan p
WHERE NOT EXISTS (
SELECT 1 FROM transaksi t WHERE t.pelanggan_id = p.id
);
Perhatikan dua hal baru di sini:
SELECT 1— angkanya tidak penting, karenaEXISTShanya peduli apakah ada baris yang cocok, bukan nilai apa yang dikembalikan.t.pelanggan_id = p.id— subquery ini merujuk kep.iddari tabel di luar. Ini membawa kita ke konsep berikutnya.
Langkah 7: Pahami Correlated Subquery
Subquery pada Langkah 6 punya sifat khusus: ia “berbicara” dengan baris dari outer query lewat p.id. Subquery seperti ini disebut correlated subquery — subquery yang nilainya bergantung pada baris outer query yang sedang diproses, sehingga ia dijalankan berulang kali, satu kali untuk setiap baris.
Bandingkan dengan pertanyaan di Langkah 2 (“gaji di atas rata-rata seluruh karyawan”) dengan pertanyaan yang lebih spesifik: “gaji di atas rata-rata divisinya masing-masing”.
sql
SELECT nama, gaji, divisi
FROM karyawan k
WHERE gaji > (
SELECT AVG(gaji)
FROM karyawan
WHERE divisi = k.divisi
);
Di sini, WHERE divisi = k.divisi membuat subquery dihitung ulang untuk tiap baris k — sekali untuk cek karyawan divisi IT, sekali untuk Finance, dan seterusnya. Hasilnya jadi lebih spesifik dibanding subquery biasa di Langkah 2.
Catatan performa: karena dieksekusi berulang, correlated subquery bisa memperlambat query pada tabel besar (jutaan baris). Selalu cek
EXPLAINatauEXPLAIN ANALYZEsebelum dipakai di lingkungan produksi.
Langkah 8: Pakai Subquery sebagai Tabel Sementara di FROM
Sampai di sini kamu sudah bisa menyaring baris dengan subquery. Langkah selanjutnya adalah memakai subquery sebagai sumber tabel, bukan hanya syarat penyaring. Ini dilakukan dengan meletakkan subquery di klausa FROM, sehingga hasilnya diperlakukan seperti tabel biasa (disebut derived table).
Contoh: cari divisi dengan rata-rata gaji di atas 8 juta.
sql
SELECT divisi, rata_rata_gaji
FROM (
SELECT divisi, AVG(gaji) AS rata_rata_gaji
FROM karyawan
GROUP BY divisi
) AS ringkasan
WHERE rata_rata_gaji > 8000000;
Cara bacanya:
- Subquery bagian dalam menghitung rata-rata gaji per divisi lebih dulu, hasilnya berupa “tabel virtual” dengan alias
ringkasan. - Query luar lalu menyaring dari tabel virtual itu, seolah-olah itu tabel sungguhan.
Pola ini berguna ketika kamu perlu “menyaring dulu, baru diolah lagi”, dan sering dipakai untuk laporan bertingkat.
Langkah 9: Gabungkan Semua Jadi Kasus Nyata
Sekarang kamu sudah punya semua building block: subquery skalar, IN, EXISTS, correlated subquery, dan subquery di FROM. Mari coba dua kasus yang lebih kompleks, gabungan dari beberapa teknik.
Mau Kuasai SQL Subquery: Kapan Dipakai dan Contoh Kasusnya Sampai Bisa Praktik Langsung?
Materi ini juga kami ajarkan langsung di kelas Edusoft Center, dibimbing mentor, sampai kamu bisa praktik nyata — bukan cuma baca teori.
Tanya Kursus via WhatsApp Lihat contoh project nyata dari siswa kami →
Kasus: mencari produk terlaris di tiap kategori, memakai subquery di FROM bersama window function.
Buat dulu tabelnya:
sql
CREATE TABLE ringkasan_penjualan (
kategori VARCHAR(50),
nama_produk VARCHAR(50),
total_terjual INT
);
INSERT INTO ringkasan_penjualan (kategori, nama_produk, total_terjual) VALUES
('Elektronik', 'Earphone A', 150),
('Elektronik', 'Charger B', 320),
('Fashion', 'Kaos C', 500),
('Fashion', 'Celana D', 210);
sql
SELECT kategori, nama_produk, total_terjual
FROM (
SELECT kategori, nama_produk, total_terjual,
RANK() OVER (PARTITION BY kategori ORDER BY total_terjual DESC) AS peringkat
FROM ringkasan_penjualan
) AS hasil_peringkat
WHERE peringkat = 1;
Subquery di FROM dibutuhkan di sini karena kita tidak bisa langsung menaruh WHERE pada hasil RANK() di level query yang sama tempat RANK() dihitung.
Kasus: menggabungkan dua subquery ringkasan bulanan dengan LEFT JOIN.
Buat dulu tabelnya:
sql
CREATE TABLE penjualan (
id INT PRIMARY KEY,
produk_id INT,
bulan VARCHAR(20),
jumlah INT
);
INSERT INTO penjualan (id, produk_id, bulan, jumlah) VALUES
(1, 101, 'Juli 2026', 50),
(2, 101, 'Agustus 2026', 80),
(3, 102, 'Juli 2026', 30),
(4, 103, 'Agustus 2026', 20);
sql
SELECT bulan_ini.produk_id,
bulan_ini.total AS penjualan_bulan_ini,
bulan_lalu.total AS penjualan_bulan_lalu,
(bulan_ini.total - COALESCE(bulan_lalu.total, 0)) AS selisih
FROM (
SELECT produk_id, SUM(jumlah) AS total
FROM penjualan
WHERE bulan = 'Agustus 2026'
GROUP BY produk_id
) AS bulan_ini
LEFT JOIN (
SELECT produk_id, SUM(jumlah) AS total
FROM penjualan
WHERE bulan = 'Juli 2026'
GROUP BY produk_id
) AS bulan_lalu
ON bulan_ini.produk_id = bulan_lalu.produk_id;
Ini menunjukkan bahwa subquery dan JOIN bukan saling menggantikan, melainkan saling melengkapi — dua subquery dipakai untuk merangkum data per bulan, lalu digabungkan dengan LEFT JOIN supaya produk tetap muncul walau salah satu bulan tidak ada penjualannya.
Langkah 10: Kapan Pilih Subquery, Kapan Pilih JOIN?
Setelah berlatih, penting untuk tahu kapan sebaiknya pakai yang mana.
Pakai JOIN ketika:
- Kamu butuh menampilkan kolom dari kedua tabel sekaligus
- Relasi datanya jelas dan tidak berisiko duplikasi
- Performa jadi prioritas — database engine modern umumnya lebih optimal menjalankan
JOINdibanding correlated subquery
Pakai subquery ketika:
- Kamu hanya perlu menyaring atau membandingkan, bukan menampilkan kolom tabel lain
- Logikanya melibatkan agregasi bertingkat
- Kamu ingin mengecek keberadaan data tanpa risiko baris terduplikasi
Sebagai catatan, banyak database modern (PostgreSQL, SQL Server versi terbaru) sudah cukup pintar mengoptimalkan subquery menjadi rencana eksekusi setara JOIN — tapi tetap cek EXPLAIN ANALYZE untuk query yang menangani data besar.
Langkah 11: Kebiasaan Baik Saat Menulis Subquery
Setelah bisa menulis berbagai jenis subquery, biasakan lima hal ini supaya query tetap mudah dibaca dan tidak membebani performa:
- Beri alias yang jelas.
AS ringkasan_penjualanjauh lebih informatif dibandingAS a. - Hindari subquery bersarang terlalu dalam. Lebih dari dua tingkat, pertimbangkan pakai Common Table Expression (
WITH ... AS). - Waspadai correlated subquery pada data besar karena dieksekusi berulang untuk tiap baris.
- Pilih
EXISTSdibandingINketika subquery mengembalikan banyak baris —EXISTSberhenti begitu menemukan satu kecocokan. - Tulis komentar yang menjelaskan niat bisnisnya, misalnya
-- ambil rata-rata gaji per divisi untuk pembanding, supaya mudah dipahami saat dibuka lagi beberapa bulan kemudian.
Langkah 12: Kesalahan Umum yang Sering Terjadi
Setelah berlatih menulis berbagai jenis subquery, ada beberapa kesalahan yang paling sering muncul, terutama bagi yang baru belajar. Mengenali pola-pola ini akan menghemat banyak waktu debugging.
1. Subquery skalar mengembalikan lebih dari satu baris
Ini adalah error paling umum. Kalau kamu menulis:
sql
SELECT nama
FROM karyawan
WHERE divisi = (SELECT divisi FROM karyawan WHERE gaji > 7000000);
Query ini akan gagal kalau ternyata ada lebih dari satu karyawan dengan gaji di atas 7 juta, karena subquery mengembalikan lebih dari satu baris sementara = hanya menerima satu nilai. Solusinya, ganti = dengan IN kalau memang subquery bisa menghasilkan banyak baris.
2. Lupa bahwa NULL bisa merusak hasil NOT IN
Ini kesalahan yang jarang disadari tapi cukup fatal. Kalau subquery di dalam NOT IN mengandung nilai NULL, seluruh hasil query bisa jadi kosong tanpa error apa pun. Buat dulu tabel contohnya:
sql
CREATE TABLE departemen_nonaktif (
divisi VARCHAR(50)
);
INSERT INTO departemen_nonaktif (divisi) VALUES
('Marketing'),
(NULL); -- baris ini yang akan merusak hasil NOT IN
Lalu coba jalankan:
sql
SELECT nama
FROM karyawan
WHERE divisi NOT IN (SELECT divisi FROM departemen_nonaktif);
Kalau tabel departemen_nonaktif punya satu saja baris dengan divisi = NULL, maka seluruh hasil query di atas akan kosong — bukan error, tapi hasilnya diam-diam salah — karena secara logika SQL, perbandingan dengan NULL menghasilkan UNKNOWN, bukan TRUE atau FALSE. Ini justru lebih berbahaya dibanding error tabel tidak ditemukan, karena tidak ada pesan yang memberitahumu bahwa ada yang salah. Solusi paling aman adalah memakai NOT EXISTS sebagai gantinya:
sql
SELECT nama
FROM karyawan k
WHERE NOT EXISTS (
SELECT 1 FROM departemen_nonaktif d WHERE d.divisi = k.divisi
);
NOT EXISTS tidak terpengaruh masalah NULL semacam ini, karena ia hanya mengecek kecocokan baris, bukan membandingkan nilai secara langsung.
3. Menaruh correlated subquery di tempat yang seharusnya cukup pakai JOIN biasa
Kadang correlated subquery dipakai padahal sebenarnya JOIN biasa sudah cukup dan jauh lebih cepat. Kalau kamu hanya butuh menampilkan data gabungan dari dua tabel tanpa ada agregasi bertingkat, coba dulu tulis versi JOIN-nya dan bandingkan EXPLAIN ANALYZE keduanya sebelum memutuskan pakai subquery.
4. Lupa alias pada derived table
Banyak database (terutama MySQL) mengharuskan subquery di klausa FROM diberi alias. Kalau lupa, biasanya muncul error semacam “every derived table must have its own alias”. Jadi pastikan selalu menutup subquery di FROM dengan AS nama_alias.
Langkah 13: Pertanyaan yang Sering Muncul
Apakah subquery selalu lebih lambat dibanding JOIN?
Tidak selalu. Untuk subquery non-correlated (yang tidak merujuk ke outer query), optimizer database modern biasanya bisa mengubahnya jadi rencana eksekusi yang setara dengan JOIN. Yang perlu diwaspadai justru correlated subquery pada tabel besar, karena berpotensi dieksekusi ribuan atau jutaan kali.
Bolehkah subquery dipakai di klausa UPDATE atau DELETE?
Boleh. Subquery sering dipakai untuk menentukan baris mana yang perlu diperbarui atau dihapus, misalnya:
sql
UPDATE karyawan
SET gaji = gaji * 1.1
WHERE divisi IN (
SELECT nama_departemen FROM departemen WHERE lokasi = 'Jakarta'
);
Query ini menaikkan gaji 10% khusus untuk karyawan yang divisinya berlokasi di Jakarta.
Apa bedanya subquery dengan Common Table Expression (CTE)?
CTE, yang ditulis dengan WITH nama_cte AS (...), pada dasarnya adalah cara lain untuk menulis subquery, tapi dengan nama yang eksplisit di awal query sehingga lebih mudah dibaca, terutama kalau subquery yang sama dipakai berkali-kali dalam satu query. Contohnya, Langkah 8 di atas bisa ditulis ulang memakai CTE seperti ini:
sql
WITH ringkasan AS (
SELECT divisi, AVG(gaji) AS rata_rata_gaji
FROM karyawan
GROUP BY divisi
)
SELECT divisi, rata_rata_gaji
FROM ringkasan
WHERE rata_rata_gaji > 8000000;
Secara fungsi hasilnya sama persis, tapi CTE membuat query lebih rapi ketika logikanya semakin kompleks dan bertingkat.
Ringkasan Alur Belajar
| Langkah | Yang Dipelajari |
|---|---|
| 1–2 | Kenapa subquery dibutuhkan, dan definisi dasarnya |
| 3 | Empat tempat subquery bisa diletakkan |
| 4 | Subquery skalar (satu nilai) |
| 5 | Subquery banyak baris dengan IN |
| 6–7 | EXISTS/NOT EXISTS dan correlated subquery |
| 8 | Subquery di FROM sebagai tabel sementara |
| 9 | Kasus gabungan yang lebih kompleks |
| 10–11 | Kapan pilih JOIN vs subquery, dan kebiasaan menulis yang baik |
| 12–13 | Kesalahan umum dan pertanyaan yang sering muncul |
Kalau kamu sudah mengikuti sampai sini, cara paling efektif untuk memperkuat pemahaman adalah mempraktikkannya langsung di database sungguhan — coba ambil satu pertanyaan dari data kerjaanmu sendiri, lalu pecah jadi dua langkah berpikir seperti di Langkah 1, dan tuliskan subquery-nya.
Penulis : Siti Fatimah Nur Azzahra
Mau Kuasai SQL Subquery: Kapan Dipakai dan Contoh Kasusnya Sampai Bisa Praktik Langsung?
Materi ini juga kami ajarkan langsung di kelas Edusoft Center, dibimbing mentor, sampai kamu bisa praktik nyata — bukan cuma baca teori.
Tanya Kursus via WhatsApp Lihat contoh project nyata dari siswa kami →
