Foto oleh panumas nikhomkhai via Pexels

Subquery, atau query di dalam query, adalah salah satu teknik paling fleksibel di SQL Server untuk menjawab pertanyaan yang tidak bisa dijawab hanya dengan satu SELECT sederhana. Subquery memungkinkan hasil dari satu query dipakai sebagai input bagi query lain, baik sebagai filter, sebagai sumber data, maupun sebagai nilai tunggal yang dibandingkan.

Ada tiga posisi utama tempat subquery biasa dipakai: di dalam klausa WHERE untuk memfilter baris berdasarkan hasil query lain, di dalam klausa FROM sebagai tabel turunan (derived table), dan di dalam SELECT sebagai scalar subquery yang menghasilkan satu nilai per baris. Memahami ketiga pola ini membuat developer bisa memilih pendekatan yang paling pas untuk kebutuhan tertentu.

Meskipun kuat, subquery juga punya jebakan performa jika dipakai sembarangan, terutama saat ditulis sebagai correlated subquery yang dieksekusi berulang kali untuk setiap baris pada query luar. Artikel ini akan membahas pola-pola aman penggunaan subquery beserta cara mengenali kapan sebaiknya diganti dengan JOIN atau CTE.

Subquery di WHERE untuk Memfilter Baris

Subquery sebagai filter IN

SELECT CustomerName FROM Customers WHERE CustomerID IN ( SELECT CustomerID FROM Orders WHERE TotalAmount > 1000000 ); -- Expected: hanya nama pelanggan yang pernah -- memiliki pesanan di atas Rp1.000.000
Pola ini sangat umum dipakai ketika kriteria filter bergantung pada data dari tabel lain yang tidak bisa langsung dimasukkan ke WHERE pada tabel utama. SQL Server akan menjalankan subquery di dalam tanda kurung terlebih dahulu, menghasilkan daftar CustomerID, lalu memakainya sebagai daftar pembanding pada klausa IN di query luar.

Scalar Subquery di Dalam SELECT

Scalar subquery menghasilkan satu nilai per baris

SELECT o.OrderID, o.TotalAmount, (SELECT AVG(TotalAmount) FROM Orders) AS RataRataSemuaPesanan FROM Orders o; -- Expected: setiap baris menampilkan TotalAmount -- masing-masing, berdampingan dengan rata-rata global
Scalar subquery wajib hanya mengembalikan satu nilai saja; jika hasilnya lebih dari satu baris, SQL Server akan melempar error saat dieksekusi. Pola ini berguna untuk menampilkan perbandingan antara nilai per baris dengan nilai agregat keseluruhan tanpa perlu menulis query terpisah atau menggabungkan dengan JOIN tambahan.

Correlated Subquery dan Dampaknya pada Performa

Correlated subquery dengan EXISTS

SELECT c.CustomerName FROM Customers c WHERE EXISTS ( SELECT 1 FROM Orders o WHERE o.CustomerID = c.CustomerID AND o.Status = 'Completed' ); -- Expected: hanya pelanggan yang punya minimal -- satu pesanan berstatus Completed
Berbeda dengan subquery biasa, correlated subquery merujuk ke kolom dari query luar di dalam kondisinya, sehingga ia dieksekusi ulang untuk setiap baris pada query luar. EXISTS sering menjadi pilihan lebih efisien dibanding IN untuk kasus ini karena mesin database bisa berhenti mencari begitu menemukan satu baris yang cocok, tanpa perlu menghitung seluruh hasil subquery.

Subquery sebagai Derived Table di FROM

Derived table memakai hasil agregat sebagai sumber filter

SELECT Kategori, TotalPenjualan FROM ( SELECT p.Category AS Kategori, SUM(od.Quantity * od.UnitPrice) AS TotalPenjualan FROM OrderDetails od JOIN Products p ON p.ProductID = od.ProductID GROUP BY p.Category ) AS Ringkasan WHERE TotalPenjualan > 5000000; -- Expected: hanya kategori dengan total penjualan -- di atas Rp5.000.000 yang ditampilkan
Pendekatan derived table berguna ketika filter yang dibutuhkan berada pada kolom hasil agregat, yang tidak bisa langsung dipakai di WHERE karena WHERE dievaluasi sebelum GROUP BY. Dengan membungkus query agregat sebagai subquery di FROM, hasil agregatnya bisa diperlakukan seperti tabel biasa dan difilter dengan WHERE pada level luar.

Subquery Berkorelasi vs Non-Korelasi dan Dampaknya pada Performa

Perbedaan eksekusi subquery non-korelasi dan subquery berkorelasi

-- Subquery NON-KORELASI: dieksekusi sekali, independen dari query luar SELECT ProductID, ProductName, Price FROM Products WHERE Price > (SELECT AVG(Price) FROM Products); -- Hasil: daftar produk dengan harga di atas rata-rata seluruh produk -- Subquery BERKORELASI: dieksekusi ulang untuk SETIAP baris query luar SELECT o.OrderID, o.CustomerID, o.OrderDate FROM Orders o WHERE o.TotalAmount > ( SELECT AVG(o2.TotalAmount) FROM Orders o2 WHERE o2.CustomerID = o.CustomerID ) -- Hasil: order yang nilainya di atas rata-rata order milik customer yang sama

Subquery non-korelasi hanya bergantung pada dirinya sendiri dan tidak memiliki referensi ke tabel pada query luar, sehingga SQL Server cukup mengeksekusinya satu kali dan menyimpan hasilnya sebagai nilai konstan yang dipakai ulang. Sebaliknya, subquery berkorelasi memiliki klausa WHERE yang merujuk pada kolom dari query luar, sehingga secara konseptual harus dievaluasi ulang untuk setiap baris yang diproses oleh query luar tersebut.

Dampak performa dari subquery berkorelasi menjadi signifikan ketika tabel luar memiliki jumlah baris yang besar, karena pola eksekusi semacam itu mirip dengan nested loop di mana subquery dijalankan berulang kali. Query optimizer SQL Server kadang mampu mentransformasi subquery berkorelasi menjadi join atau menggunakan apply operator yang lebih efisien, namun transformasi ini tidak selalu terjadi, terutama pada query dengan logika agregasi bersyarat yang kompleks.

Sebagai alternatif, banyak subquery berkorelasi yang melakukan agregasi per grup dapat ditulis ulang menggunakan JOIN terhadap hasil GROUP BY, atau memakai window function seperti AVG() OVER (PARTITION BY CustomerID), yang pada contoh di atas akan menghasilkan rencana eksekusi dengan satu kali pemindaian data ketimbang pemanggilan subquery berulang.

Perbedaan EXISTS dan IN pada Subquery serta Dampaknya

IN menghasilkan daftar nilai, EXISTS hanya menguji keberadaan baris

-- Memakai IN: SQL Server umumnya membangun daftar nilai lalu mencocokkan SELECT CustomerID, CustomerName FROM Customers c WHERE c.CustomerID IN (SELECT o.CustomerID FROM Orders o WHERE o.TotalAmount > 5000000); -- Memakai EXISTS: berhenti begitu satu baris cocok ditemukan (short-circuit) SELECT CustomerID, CustomerName FROM Customers c WHERE EXISTS ( SELECT 1 FROM Orders o WHERE o.CustomerID = c.CustomerID AND o.TotalAmount > 5000000 ); -- Hasil kedua query sama persis, tetapi rencana eksekusinya bisa berbeda -- terutama saat subquery mengembalikan NULL

IN dan EXISTS sering dianggap setara karena pada banyak kasus optimizer SQL Server memang mentransformasi keduanya menjadi rencana eksekusi yang identik, biasanya berupa semi join. Namun secara konseptual IN membangun atau membandingkan terhadap kumpulan nilai hasil subquery, sedangkan EXISTS hanya memeriksa apakah subquery mengembalikan minimal satu baris, tanpa peduli nilai kolom yang dikembalikan subquery tersebut.

Perbedaan paling signifikan muncul ketika daftar hasil subquery pada IN mengandung NULL. Jika subquery pada klausa WHERE CustomerID IN (...) mengembalikan NULL di antara nilainya, dan kolom CustomerID pada baris luar tidak cocok dengan nilai mana pun, hasil perbandingan menjadi UNKNOWN yang dapat membuat seluruh kondisi NOT IN gagal mengembalikan baris yang seharusnya muncul. EXISTS tidak memiliki masalah ini karena ia hanya mengevaluasi keberadaan baris, bukan membandingkan nilai satu per satu.

Dari sisi performa, EXISTS umumnya lebih aman dipakai pada subquery berkorelasi karena optimizer bisa langsung berhenti mencari begitu satu baris cocok ditemukan, sementara NOT IN dengan subquery yang berpotensi mengandung NULL sebaiknya dihindari dan diganti NOT EXISTS, karena NOT IN dengan NULL di dalam daftar dapat membuat query secara diam-diam tidak mengembalikan baris apa pun tanpa pesan error. Kebiasaan menambahkan kondisi IS NOT NULL secara eksplisit pada kolom subquery sebelum memakai NOT IN juga menjadi langkah pencegahan yang umum disarankan ketika EXISTS tidak memungkinkan dipakai pada struktur query tertentu.

Scalar Subquery pada SELECT List untuk Menyisipkan Nilai Agregat

Scalar subquery pada SELECT list, dieksekusi per baris mirip subquery berkorelasi

-- Scalar subquery di SELECT list harus mengembalikan tepat satu nilai SELECT p.ProductID, p.ProductName, p.Price, (SELECT AVG(p2.Price) FROM Products p2 WHERE p2.CategoryID = p.CategoryID) AS AvgHargaKategori FROM Products p; -- Hasil: tiap baris produk disertai rata-rata harga kategorinya sendiri -- Jika subquery berpotensi mengembalikan lebih dari satu baris, query akan ERROR saat dijalankan -- Msg 512: Subquery returned more than 1 value.

Scalar subquery yang ditulis langsung di dalam SELECT list berfungsi menyisipkan satu nilai tunggal ke setiap baris hasil, berbeda dari subquery pada klausa WHERE yang dipakai untuk menyaring baris. Subquery semacam ini wajib mengembalikan paling banyak satu baris dan satu kolom, karena SQL Server akan menghentikan eksekusi dengan error jika subquery tersebut mengembalikan lebih dari satu nilai pada baris mana pun.

Karena subquery pada SELECT list umumnya berkorelasi dengan kolom dari tabel luar, pola eksekusinya serupa dengan subquery berkorelasi pada WHERE, yaitu berpotensi dievaluasi ulang untuk setiap baris yang diproses. Pada tabel dengan jumlah baris besar, pola ini dapat ditulis ulang memakai window function seperti AVG() OVER (PARTITION BY CategoryID) agar perhitungan agregat cukup dilakukan dalam satu kali pemindaian data. Rekomendasi umum adalah membandingkan execution plan kedua bentuk query tersebut pada volume data produksi sebelum memutuskan mana yang dipertahankan dalam kode aplikasi jangka panjang.

Kesalahan Umum yang Sering Terjadi

Memakai Correlated Subquery untuk Dataset Besar

Correlated subquery yang dieksekusi ribuan kali untuk dataset besar bisa membuat query menjadi sangat lambat dibanding mengubahnya menjadi JOIN atau menggunakan window function, terutama ketika tabel luar punya jutaan baris.

Lupa Scalar Subquery Hanya Boleh Satu Nilai

Menulis scalar subquery yang berpotensi mengembalikan lebih dari satu baris akan menyebabkan error runtime yang baru terlihat saat data bertambah, bukan saat query pertama kali ditulis dan diuji dengan data kecil.

Menggunakan IN dengan Subquery yang Bisa Menghasilkan NULL

Jika subquery di dalam NOT IN menghasilkan nilai NULL pada salah satu barisnya, seluruh hasil NOT IN bisa menjadi kosong tanpa pesan error apa pun, sebuah perilaku yang sering membingungkan developer pemula.

Kapan Pendekatan Ini Tidak Cocok Dipakai

Subquery sangat baik untuk query yang sifatnya sekali pakai atau logikanya cukup sederhana untuk dipahami dalam satu blok, namun untuk query yang kompleks dengan banyak langkah, CTE biasanya lebih mudah dibaca dan dipelihara karena setiap langkah bisa diberi nama yang jelas.

Dari sisi performa, query optimizer SQL Server modern umumnya cukup pintar mengubah subquery menjadi bentuk JOIN secara internal, sehingga perbedaan performa antara subquery dan JOIN untuk kasus non-correlated seringkali tidak signifikan — pengecualian utamanya ada pada correlated subquery yang memang secara desain dieksekusi berulang.

Ringkasan

Subquery adalah alat yang fleksibel untuk memecah masalah kompleks menjadi query-query kecil yang saling bersarang, baik untuk memfilter, membandingkan, maupun menyediakan data turunan. Pilih IN atau EXISTS berdasarkan apakah subquery berkorelasi dengan query luar, dan pertimbangkan CTE jika logika subquery mulai terlalu panjang untuk dibaca dalam satu blok.

Seri Belajar SQL Server:

← Sebelumnya: Menggabungkan Query dengan UNION dan UNION ALL

→ Selanjutnya: Common Table Expression (CTE) di SQL Server (segera terbit)