CTE vs Subquery vs View: Kapan Pakai yang Mana di SQL
Perbandingan CTE, subquery, view, dan temp table di SQL: kelebihan, kekurangan, dan kapan memilih masing-masing. Plus recursive CTE dan mitos performanya.
Pembuat Hyper Sheets & BelajarWeb.dev
Pilih subquery kalau logikanya kecil dan hanya dipakai sekali, pilih CTE kalau hasil antaranya dipakai lebih dari sekali atau nesting-nya sudah lebih dari satu tingkat, dari sisi performa keduanya umumnya setara, jadi ini keputusan keterbacaan. View dan temp table punya pertimbangan berbeda lagi. Artikel ini membandingkan keempatnya, menjelaskan sintaks WITH, multiple CTE, WITH RECURSIVE untuk data berjenjang, dan enam kesalahan CTE yang paling umum.
CTE (Common Table Expression) adalah hasil query sementara yang diberi nama dan bisa dipakai ulang di dalam satu statement SQL, didefinisikan dengan kata kunci WITH sebelum SELECT utama, dan hidup hanya selama query itu berjalan. Begitu query selesai dieksekusi, CTE lenyap: ia tidak tersimpan di database seperti tabel atau view. Anggap saja CTE sebagai “variabel bernama” untuk potongan query, supaya query panjang bisa dipecah jadi langkah-langkah yang terbaca.
Kalau kamu pernah menatap subquery bersarang tiga tingkat dan menyerah memahaminya, artikel ini untukmu. Kita bahas berurutan: sintaks dasar WITH, kenapa CTE ada (dengan refactor nyata dari subquery yang tidak terbaca), cara menulis beberapa CTE sekaligus, perbandingan CTE vs subquery vs view vs temp table, recursive CTE untuk data berjenjang, sampai walkthrough analisis data penjualan bertahap. Semua contoh SQL di sini runnable, skema tabelnya kecil dan disertakan lengkap, tinggal copy-paste ke MySQL 8, PostgreSQL, atau SQLite.
CTE SQL Adalah… (Definisi dan Sintaks Dasar WITH)
CTE adalah named result set, hasil query yang diberi nama, yang didefinisikan dengan kata kunci WITH di awal sebuah statement, lalu dipakai seperti tabel biasa di query utama setelahnya. Namanya “common table expression” karena ia berperilaku seperti tabel (bisa di-SELECT, di-JOIN, di-filter), tapi sebenarnya hanyalah ekspresi sementara yang umurnya satu query. Sintaks dasarnya begini:
WITH nama_cte AS (
-- query apa pun yang menghasilkan tabel
SELECT kolom1, kolom2
FROM tabel_sumber
WHERE kondisi
)
SELECT *
FROM nama_cte;
Tiga bagian yang perlu kamu hafal:
WITH nama_cte AS (...), mendefinisikan CTE. Isi di dalam kurung adalah querySELECTbiasa; hasilnya “dibungkus” dan diberi namanama_cte.- Query utama, statement
SELECT(atauINSERT/UPDATE/DELETEdi banyak database) yang ditulis setelah definisi CTE. Di sinilah kamu memakainama_cteseolah-olah ia tabel sungguhan. - Masa hidup, CTE hanya dikenali di dalam statement yang sama. Jalankan query berikutnya, dan
nama_ctesudah tidak ada.
Supaya semua contoh di artikel ini bisa kamu jalankan sendiri, kita pakai dua tabel kecil: produk dan penjualan. Buat dulu skemanya:
CREATE TABLE produk (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
kategori TEXT NOT NULL,
harga INTEGER NOT NULL
);
CREATE TABLE penjualan (
id INTEGER PRIMARY KEY,
produk_id INTEGER NOT NULL,
jumlah INTEGER NOT NULL,
tanggal DATE NOT NULL
);
INSERT INTO produk (id, nama, kategori, harga) VALUES
(1, 'Kaos Polos', 'Pakaian', 75000),
(2, 'Hoodie', 'Pakaian', 250000),
(3, 'Botol Minum', 'Aksesoris', 90000),
(4, 'Topi', 'Aksesoris', 60000),
(5, 'Jaket Denim', 'Pakaian', 400000);
INSERT INTO penjualan (id, produk_id, jumlah, tanggal) VALUES
(1, 1, 3, '2026-07-01'),
(2, 2, 1, '2026-07-01'),
(3, 1, 2, '2026-07-02'),
(4, 3, 5, '2026-07-03'),
(5, 4, 2, '2026-07-04'),
(6, 2, 2, '2026-07-05'),
(7, 5, 1, '2026-07-06'),
(8, 1, 4, '2026-07-07'),
(9, 3, 1, '2026-07-08'),
(10, 4, 3, '2026-07-09');
Sekarang contoh CTE pertama yang nyata: hitung total unit terjual per produk selama Juli, lalu tampilkan bersama nama produknya.
WITH penjualan_juli AS (
SELECT produk_id, SUM(jumlah) AS total_terjual
FROM penjualan
WHERE tanggal BETWEEN '2026-07-01' AND '2026-07-31'
GROUP BY produk_id
)
SELECT p.nama, pj.total_terjual
FROM penjualan_juli pj
JOIN produk p ON p.id = pj.produk_id
ORDER BY pj.total_terjual DESC;
Hasilnya: Kaos Polos 9 unit, Botol Minum 6, Topi 5, Hoodie 3, Jaket Denim 1. Perhatikan alurnya, bagian agregasi diberi nama penjualan_juli yang menjelaskan apa isinya, lalu query utama tinggal melakukan JOIN ke tabel produk. Query terbaca dari atas ke bawah seperti resep masakan: siapkan bahan dulu, baru olah. Kalau konsep JOIN-nya sendiri masih terasa abu-abu, lesson INNER JOIN di Hyper Sheets membahasnya dari nol dengan latihan langsung di browser, dan untuk CTE-nya sendiri ada lesson CTE interaktif yang bisa kamu kerjakan setelah membaca artikel ini.
Kenapa CTE Ada? Subquery Bersarang vs CTE Berjenjang
CTE ada untuk menyelamatkan keterbacaan: query kompleks yang ditulis dengan subquery bersarang harus dibaca dari dalam ke luar, sedangkan CTE membalik arahnya jadi atas ke bawah, searah dengan cara manusia berpikir. Bonus keduanya: satu CTE bisa dirujuk berkali-kali dalam query yang sama, sementara subquery harus di-copy-paste tiap kali dibutuhkan. Dua masalah inilah yang bikin WITH masuk ke standar SQL.
Biar tidak abstrak, kita refactor kasus nyata. Soal: tampilkan produk yang pendapatannya di atas rata-rata pendapatan semua produk. Versi subquery bersarang:
-- Versi subquery: dibaca dari dalam ke luar, dan logika
-- yang sama ditulis DUA KALI
SELECT nama, pendapatan
FROM (
SELECT p.nama, SUM(pj.jumlah * p.harga) AS pendapatan
FROM penjualan pj
JOIN produk p ON p.id = pj.produk_id
GROUP BY p.nama
) AS pendapatan_produk
WHERE pendapatan > (
SELECT AVG(pendapatan)
FROM (
SELECT SUM(pj.jumlah * p.harga) AS pendapatan
FROM penjualan pj
JOIN produk p ON p.id = pj.produk_id
GROUP BY p.nama
) AS x
);
Query ini benar, tapi punya dua penyakit. Pertama, untuk memahaminya kamu harus menyelam ke subquery terdalam dulu, naik satu tingkat, lalu lompat ke subquery kedua di klausa WHERE. Kedua, dan ini yang lebih parah, perhitungan pendapatan per produk ditulis dua kali persis sama. Kalau besok definisi “pendapatan” berubah (misalnya dikurangi diskon), kamu harus ingat mengubah keduanya. Lupa satu, hasilnya salah diam-diam.
Sekarang versi CTE:
-- Versi CTE: dibaca dari atas ke bawah, logika ditulis SEKALI
WITH pendapatan_produk AS (
SELECT p.nama, SUM(pj.jumlah * p.harga) AS pendapatan
FROM penjualan pj
JOIN produk p ON p.id = pj.produk_id
GROUP BY p.nama
),
rata_rata AS (
SELECT AVG(pendapatan) AS nilai
FROM pendapatan_produk
)
SELECT nama, pendapatan
FROM pendapatan_produk
WHERE pendapatan > (SELECT nilai FROM rata_rata);
Logika pendapatan hanya ditulis sekali, lalu dipakai dua kali: sekali untuk dihitung rata-ratanya di CTE rata_rata, sekali lagi di query utama. Alurnya linier: hitung pendapatan per produk → hitung rata-ratanya → saring yang di atas rata-rata. Dengan data contoh kita, rata-rata pendapatannya 533.000, jadi yang lolos adalah Hoodie (750.000), Kaos Polos (675.000), dan Botol Minum (540.000).
Subquery tetap punya tempat untuk kasus kecil, perbandingannya kita bedah beberapa bagian lagi, dan kalau kamu mau mendalami subquery itu sendiri, ada lesson subquery yang membahas semua variannya.
Multiple CTE dalam Satu Query
Kamu bisa mendefinisikan beberapa CTE dalam satu statement dengan memisahkannya pakai koma, WITH a AS (...), b AS (...), dan kata kunci WITH hanya ditulis sekali di awal. Aturan pentingnya: CTE yang didefinisikan belakangan boleh merujuk CTE sebelumnya, tapi tidak sebaliknya. Urutan penulisan = urutan ketergantungan.
Contoh: hitung total unit terjual per produk, lalu gulung lagi jadi total per kategori.
WITH penjualan_per_produk AS (
SELECT produk_id, SUM(jumlah) AS total_terjual
FROM penjualan
GROUP BY produk_id
),
penjualan_per_kategori AS (
-- CTE kedua boleh membaca CTE pertama
SELECT p.kategori, SUM(pp.total_terjual) AS total_kategori
FROM penjualan_per_produk pp
JOIN produk p ON p.id = pp.produk_id
GROUP BY p.kategori
)
SELECT kategori, total_kategori
FROM penjualan_per_kategori
ORDER BY total_kategori DESC;
Hasilnya: Pakaian 13 unit, Aksesoris 11 unit. Perhatikan tiga detail sintaks yang sering bikin error:
WITHhanya sekali di paling awal. MenulisWITHlagi sebelum CTE kedua adalah syntax error.- Antar CTE dipisah koma, setelah kurung tutup CTE pertama, sebelum nama CTE kedua.
- Setelah CTE terakhir tidak ada koma, langsung lanjut ke query utama.
Pola “CTE berjenjang” seperti ini adalah cara paling umum menyusun query analitik: setiap CTE adalah satu tahap transformasi, dan kamu bisa men-debug per tahap dengan mengganti query utama jadi SELECT * FROM nama_cte untuk mengintip hasil antara. Coba sendiri: ganti query utama di atas jadi SELECT * FROM penjualan_per_produk dan kamu langsung lihat isi tahap pertama. Teknik debugging ini saja sudah sepadan dengan waktu yang kamu habiskan belajar CTE.
CTE vs Subquery: Kapan Pilih yang Mana?
Perbedaan CTE dan subquery: CTE didefinisikan sekali di awal dengan nama dan bisa dirujuk berkali-kali dalam satu statement, sedangkan subquery ditulis inline di tempat ia dipakai dan tidak bisa direuse. Dari sisi hasil, keduanya setara, apa pun yang bisa ditulis dengan CTE non-recursive bisa ditulis dengan subquery, dan sebaliknya. Yang berbeda adalah keterbacaan dan kemudahan perawatan.
Panduan praktisnya:
- Pakai subquery kalau logikanya kecil, hanya dipakai sekali, dan justru lebih jelas ditulis inline. Contoh:
WHERE harga > (SELECT AVG(harga) FROM produk), memecah ini jadi CTE malah menambah upacara tanpa manfaat. - Pakai CTE kalau hasil antara dipakai lebih dari sekali, kalau nesting-nya mulai lebih dari satu tingkat, atau kalau memberi nama pada langkah itu membuat maksud query lebih jelas.
- Pakai CTE juga saat kamu butuh recursive query, subquery tidak bisa memanggil dirinya sendiri,
WITH RECURSIVEbisa. Ini kemampuan yang benar-benar eksklusif milik CTE, kita bahas sebentar lagi.
Bagaimana dengan performa? Umumnya setara. Di database modern, query planner (optimizer) memperlakukan CTE non-recursive mirip dengan derived table/subquery: ia bebas meng-inline definisi CTE ke query utama dan mengoptimalkannya sebagai satu kesatuan. Jadi memilih CTE vs subquery adalah keputusan keterbacaan, bukan keputusan performa.
Intinya: jangan pilih subquery demi “performa” tanpa mengukur, tulis versi yang paling terbaca dulu, cek rencana eksekusinya dengan EXPLAIN kalau memang terasa lambat.
Soal dukungan database: MySQL 8.0 ke atas, PostgreSQL, SQLite modern, SQL Server, dan Oracle semuanya mendukung CTE. Yang perlu diwaspadai satu: MySQL 5.7 dan yang lebih lama tidak mendukung WITH, kalau kamu menjalankan contoh artikel ini di MySQL 5.7 dan dapat syntax error, itu sebabnya, dan solusinya upgrade ke MySQL 8 (atau tulis ulang sebagai derived table). Untuk belajar dan eksperimen, SQLite adalah pilihan paling ringan: tanpa instalasi server, dan semua contoh di artikel ini jalan di sana.
CTE vs View vs Temp Table: Kapan Pilih yang Mana?
Bedanya ada di masa hidup: CTE hidup selama satu query, view tersimpan permanen sebagai definisi query di database, dan temp table menyimpan data sungguhan selama satu sesi koneksi. Ketiganya sama-sama memberi “nama” pada hasil query, tapi untuk kebutuhan yang berbeda. Salah pilih tidak membuat query-mu error, hanya membuat hidupmu lebih repot dari seharusnya.
Peta keputusannya:
| Fitur | CTE | View | Temp Table |
|---|---|---|---|
| Masa hidup | Satu statement | Permanen (sampai di-DROP) |
Satu sesi koneksi |
| Menyimpan data? | Tidak | Tidak (hanya definisi query) | Ya, data sungguhan |
| Bisa dipakai lintas query? | Tidak | Ya | Ya (dalam sesi yang sama) |
| Bisa diberi index? | Tidak | Tidak (view biasa) | Ya |
| Butuh permission DDL? | Tidak | Ya (CREATE VIEW) |
Biasanya ya |
Kapan pilih yang mana:
- CTE, untuk menstrukturkan satu query yang kompleks. Kebutuhan sekali pakai, tidak meninggalkan jejak apa pun di database, tidak butuh izin khusus. Ini pilihan default untuk query analitik sehari-hari.
- View, untuk logika yang dipakai berulang di banyak query atau banyak aplikasi. Misalnya definisi “pendapatan bersih” yang dipakai tim finance di lima laporan berbeda: simpan sekali sebagai view, semua laporan merujuk ke sana, dan perubahan definisi cukup di satu tempat. Detail cara membuat dan memakainya ada di lesson view SQL.
- Temp table, untuk hasil antara yang mahal dihitung dan akan dipakai oleh beberapa statement berbeda dalam satu sesi. Karena datanya benar-benar disimpan (dan bisa di-index), kamu bayar biaya hitung sekali lalu query berkali-kali dengan cepat.
Recursive CTE: WITH RECURSIVE untuk Data Berjenjang
Recursive CTE adalah CTE yang merujuk dirinya sendiri, dipakai untuk data berjenjang atau berulang: hierarki kategori, struktur organisasi, deret tanggal, atau graf. Strukturnya selalu tiga bagian: anchor member (baris awal), UNION ALL, dan recursive member (aturan untuk menghasilkan baris berikutnya dari baris sebelumnya), plus kondisi berhenti supaya tidak berputar selamanya. Di PostgreSQL, MySQL, dan SQLite kata kuncinya WITH RECURSIVE; di SQL Server cukup WITH biasa.
Mulai dari contoh terkecil yang mungkin, mencetak angka 1 sampai 10:
WITH RECURSIVE angka AS (
SELECT 1 AS n -- anchor member: baris pertama
UNION ALL
SELECT n + 1 -- recursive member: baris berikutnya
FROM angka -- ...dihitung dari baris sebelumnya
WHERE n < 10 -- kondisi berhenti
)
SELECT n FROM angka;
Cara membacanya: anchor member menghasilkan baris awal (n = 1). Lalu recursive member dijalankan berulang, setiap putaran menerima baris hasil putaran sebelumnya, menghasilkan n + 1, dan berhenti ketika kondisi WHERE n < 10 tidak lagi menghasilkan baris baru. Hasil akhirnya gabungan semua putaran: angka 1 sampai 10. Tanpa klausa WHERE itu, query akan mencoba berjalan selamanya (untungnya kebanyakan database punya batas kedalaman rekursi yang akhirnya menghentikannya dengan error).
Sekarang contoh yang lebih nyata: hierarki kategori produk ala marketplace, di mana setiap kategori bisa punya induk. Buat tabelnya:
CREATE TABLE kategori (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
parent_id INTEGER -- NULL berarti kategori paling atas
);
INSERT INTO kategori (id, nama, parent_id) VALUES
(1, 'Elektronik', NULL),
(2, 'Komputer', 1),
(3, 'Laptop', 2),
(4, 'Laptop Gaming', 3),
(5, 'Aksesoris Komputer', 2),
(6, 'Fashion', NULL);
Soal klasiknya: tampilkan seluruh pohon kategori beserta kedalamannya. Dengan JOIN biasa kamu harus tahu dulu berapa tingkat maksimalnya, dan menulis satu JOIN per tingkat. Recursive CTE tidak peduli berapa dalam pohonnya:
WITH RECURSIVE pohon_kategori AS (
-- anchor: mulai dari kategori paling atas
SELECT id, nama, parent_id, 1 AS level
FROM kategori
WHERE parent_id IS NULL
UNION ALL
-- recursive: ambil anak dari baris yang sudah ditemukan
SELECT k.id, k.nama, k.parent_id, pk.level + 1
FROM kategori k
JOIN pohon_kategori pk ON k.parent_id = pk.id
)
SELECT level, nama
FROM pohon_kategori
ORDER BY level, id;
Hasilnya: level 1 berisi Elektronik dan Fashion; level 2 berisi Komputer; level 3 berisi Laptop dan Aksesoris Komputer; level 4 berisi Laptop Gaming. Kondisi berhentinya di sini implisit: rekursi berhenti sendiri ketika tidak ada lagi baris kategori yang parent_id-nya cocok dengan baris hasil putaran sebelumnya, alias ketika semua daun pohon sudah ditemukan. Pola yang sama persis dipakai untuk struktur organisasi (karyawan → atasan), thread komentar berbalas, sampai perhitungan rute.
Dua rambu-rambu saat menulis recursive CTE:
Praktik: Analisis Data Penjualan Bertahap dengan CTE
Pola paling produktif memakai CTE adalah pipeline tiga tahap: filter data mentah, agregasi, lalu ranking, masing-masing jadi satu CTE bernama. Di bagian ini kita rangkai semuanya jadi satu query analitik utuh yang menjawab pertanyaan bisnis nyata: “Selama Juli, apa dua produk dengan pendapatan tertinggi di tiap kategori?” Kita tetap memakai tabel produk dan penjualan yang sudah dibuat di awal artikel, kalau kamu melompat ke sini, jalankan dulu CREATE TABLE dan INSERT di bagian definisi.
Ini query lengkapnya:
WITH penjualan_juli AS (
-- Tahap 1: FILTER, ambil hanya baris yang relevan
SELECT produk_id, jumlah
FROM penjualan
WHERE tanggal BETWEEN '2026-07-01' AND '2026-07-31'
),
pendapatan_produk AS (
-- Tahap 2: AGREGASI, rangkum per produk
SELECT
p.id,
p.nama,
p.kategori,
SUM(pj.jumlah) AS total_terjual,
SUM(pj.jumlah * p.harga) AS pendapatan
FROM penjualan_juli pj
JOIN produk p ON p.id = pj.produk_id
GROUP BY p.id, p.nama, p.kategori
),
peringkat AS (
-- Tahap 3: RANKING, beri peringkat di dalam tiap kategori
SELECT
nama,
kategori,
total_terjual,
pendapatan,
RANK() OVER (
PARTITION BY kategori
ORDER BY pendapatan DESC
) AS peringkat_kategori
FROM pendapatan_produk
)
SELECT nama, kategori, total_terjual, pendapatan, peringkat_kategori
FROM peringkat
WHERE peringkat_kategori <= 2
ORDER BY kategori, peringkat_kategori;
Hasil dengan data contoh kita:
| nama | kategori | total_terjual | pendapatan | peringkat_kategori |
|---|---|---|---|---|
| Botol Minum | Aksesoris | 6 | 540000 | 1 |
| Topi | Aksesoris | 5 | 300000 | 2 |
| Hoodie | Pakaian | 3 | 750000 | 1 |
| Kaos Polos | Pakaian | 9 | 675000 | 2 |
Bedah per tahap:
Tahap 1, filter (penjualan_juli). Kita persempit data seawal mungkin: hanya transaksi Juli, hanya kolom yang dibutuhkan. Kebiasaan bagus untuk keterbacaan (pembaca langsung tahu scope datanya) sekaligus memberi optimizer sinyal jelas tentang data mana yang relevan.
Tahap 2, agregasi (pendapatan_produk). GROUP BY merangkum baris transaksi jadi satu baris per produk, dengan SUM(jumlah) untuk unit dan SUM(jumlah * harga) untuk pendapatan. Tahap ini bergantung pada tahap 1, perhatikan FROM penjualan_juli, bukan FROM penjualan. Kalau perilaku GROUP BY (kenapa semua kolom non-agregat harus masuk ke daftar GROUP BY, misalnya) masih sering bikin error di query-mu, lesson GROUP BY melatihnya bertahap dengan kasus-kasus yang mirip contoh ini.
Tahap 3, ranking (peringkat). RANK() OVER (PARTITION BY kategori ORDER BY pendapatan DESC) memberi peringkat 1, 2, 3, … di dalam masing-masing kategori tanpa menggabungkan barisnya. Ini window function, keluarga fungsi yang sangat sering berpasangan dengan CTE, karena window function tidak bisa dipakai langsung di klausa WHERE. Perhatikan triknya: kita hitung RANK() di dalam CTE, lalu baru memfilternya (WHERE peringkat_kategori <= 2) di query utama. Tanpa CTE (atau subquery), filter “ambil top 2 per kategori” ini tidak bisa ditulis. Kalau OVER, PARTITION BY, dan kawan-kawannya masih asing, lesson window functions membahasnya dari nol, dan setelah paham keduanya, kombinasi CTE + window function akan jadi senjata analisis data paling sering kamu pakai.
Yang layak dicatat dari walkthrough ini: setiap tahap bisa diuji sendiri-sendiri. Saat hasilnya aneh, ganti query utama jadi SELECT * FROM pendapatan_produk untuk memeriksa hasil agregasi sebelum ranking. Pipeline bernama = titik pemeriksaan gratis di setiap tahap. Inilah alasan kenapa di dunia kerja, query laporan yang panjang hampir selalu ditulis dengan gaya CTE berjenjang seperti ini, bukan subquery bersarang.
Kesalahan Umum Saat Menggunakan CTE
Kesalahan paling umum seputar CTE: salah taruh koma antar CTE, mengira CTE tersimpan permanen, dan menulis recursive CTE tanpa kondisi berhenti. Berikut daftar lengkapnya, sebagian besar error CTE yang akan kamu temui di minggu-minggu pertama ada di sini, lengkap dengan solusinya.
1. Lupa koma antar CTE (atau menulis WITH dua kali)
Sintaks multiple CTE itu: WITH sekali di awal, koma di antara definisi, dan tanpa koma setelah definisi terakhir. Dua-duanya sering kebalik:
-- SALAH: WITH ditulis dua kali
WITH a AS (SELECT 1 AS x)
WITH b AS (SELECT 2 AS y) -- syntax error di sini
SELECT * FROM a, b;
-- BENAR: satu WITH, pisahkan dengan koma
WITH a AS (SELECT 1 AS x),
b AS (SELECT 2 AS y)
SELECT * FROM a, b;
Kalau kamu dapat syntax error tepat di sekitar kata WITH kedua atau di kurung tutup sebuah CTE, 90% masalahnya koma ini.
2. Mengira CTE tersimpan permanen
CTE bukan view dan bukan tabel, ia mati begitu statement-nya selesai. Menjalankan SELECT * FROM nama_cte sebagai query terpisah setelahnya akan error “table not found”. Kalau kamu butuh hasil yang bertahan antar query, buat view (untuk logika yang dipakai berulang) atau temp table (untuk data antar-statement dalam satu sesi). CTE hanya untuk konsumsi query itu sendiri.
3. CTE didefinisikan tapi tidak pernah dipakai
Ini biasanya bukan error, kebanyakan database membiarkannya, tapi tetap masalah. Pertama, ia membingungkan pembaca: ada langkah bernama yang tidak berkontribusi apa-apa ke hasil. Kedua, tergantung database dan versi, CTE yang tidak dirujuk bisa saja tetap ikut dievaluasi, kerja sia-sia. Ini sering terjadi sisa refactor: kamu mengubah query utama tapi lupa membuang CTE lama. Biasakan membaca ulang daftar CTE setelah selesai dan hapus yang tidak dirujuk.
4. Recursive CTE tanpa kondisi berhenti
Recursive member yang selalu menghasilkan baris baru = loop tak berhingga. Database modern punya pagar pengaman (batas kedalaman rekursi yang bisa dikonfigurasi), jadi biasanya kamu dapat error, bukan server hang, tapi error itu tetap berarti query-mu gagal. Pastikan selalu ada salah satu dari: kondisi WHERE eksplisit di recursive member (seperti WHERE n < 10 di contoh deret angka), atau struktur data yang dijamin tidak bersiklus (pohon kategori yang sehat). Kalau datamu berpotensi punya siklus, tambahkan kolom penanda jalur atau batas level eksplisit seperti WHERE pk.level < 20.
5. Memakai CTE untuk hal yang JOIN sederhana sudah cukup
CTE itu alat keterbacaan, tapi keterbacaan juga bisa rusak karena terlalu banyak upacara. Query dua tabel yang selesai dengan satu INNER JOIN tidak perlu dibungkus tiga CTE:
-- Berlebihan: CTE untuk sesuatu yang trivial
WITH semua_penjualan AS (
SELECT * FROM penjualan
)
SELECT p.nama, s.jumlah
FROM semua_penjualan s
JOIN produk p ON p.id = s.produk_id;
-- Cukup: JOIN langsung
SELECT p.nama, pj.jumlah
FROM penjualan pj
JOIN produk p ON p.id = pj.produk_id;
Aturan praktisnya: kalau CTE-nya cuma SELECT * FROM satu_tabel tanpa transformasi apa pun, buang. CTE baru bernilai ketika ia merangkum satu langkah berpikir, filter bermakna, agregasi, atau perhitungan yang layak diberi nama.
6. Menganggap CTE otomatis lebih cepat (atau lebih lambat)
CTE bukan jurus optimasi dan bukan juga beban. Untuk kasus non-recursive, optimizer umumnya memperlakukannya setara subquery. Kalau query-mu lambat, penyebabnya hampir selalu di tempat lain: index yang hilang, filter yang kurang selektif, atau data yang memang besar. Ukur dengan EXPLAIN sebelum menyalahkan (atau mengandalkan) CTE.
Untuk mengambil 2 produk dengan pendapatan tertinggi per kategori, kenapa RANK() dihitung di dalam CTE lalu baru difilter di query utama?
FAQ Seputar CTE SQL
CTE SQL adalah apa?
CTE (Common Table Expression) adalah hasil query sementara yang diberi nama, didefinisikan dengan kata kunci WITH di awal sebuah statement SQL, dan bisa dipakai seperti tabel biasa di query utama setelahnya. CTE hanya hidup selama statement itu berjalan, tidak tersimpan di database. Fungsinya terutama untuk memecah query kompleks jadi langkah-langkah bernama yang mudah dibaca.
Apa fungsi WITH di SQL?
WITH adalah kata kunci untuk mendefinisikan CTE: WITH nama AS (query) SELECT ... FROM nama. Kamu bisa mendefinisikan beberapa CTE sekaligus dengan memisahkannya pakai koma, dan varian WITH RECURSIVE dipakai untuk query rekursif seperti menelusuri data hierarki. WITH ditulis hanya sekali di paling awal statement, sebelum SELECT utama.
Apa beda CTE dan subquery?
CTE didefinisikan di awal statement dengan nama dan bisa dirujuk berkali-kali dalam query yang sama, sedangkan subquery ditulis inline di tempat ia dipakai dan harus di-copy-paste kalau dibutuhkan lagi. CTE dibaca dari atas ke bawah (lebih terbaca untuk query kompleks), subquery dari dalam ke luar. Kemampuan yang hanya dimiliki CTE: rekursi lewat WITH RECURSIVE. Untuk logika kecil sekali pakai, subquery sering justru lebih ringkas.
Apa beda CTE dan view?
CTE bersifat sementara, hidup hanya selama satu query dan tidak meninggalkan apa pun di database. View tersimpan permanen sebagai definisi query bernama di database, bisa dipakai oleh query dan aplikasi mana pun, dan baru hilang kalau di-DROP. Pakai CTE untuk menstrukturkan satu query; pakai view untuk logika yang dipakai berulang di banyak tempat.
Apakah CTE lebih cepat dari subquery?
Umumnya tidak, performanya setara, karena optimizer database modern memperlakukan CTE non-recursive mirip dengan subquery dan bebas meng-inline-nya. Pilihlah berdasarkan keterbacaan, bukan performa. Pengecualian yang layak diketahui: PostgreSQL sebelum versi 12 selalu mematerialisasi CTE sehingga kadang lebih lambat; sejak versi 12 perilakunya sudah setara. Kalau query lambat, cek rencana eksekusi dengan EXPLAIN, biang keroknya hampir selalu bukan CTE-nya.
Database apa saja yang mendukung CTE?
MySQL 8.0 ke atas, PostgreSQL, SQLite modern, SQL Server, MariaDB, dan Oracle semuanya mendukung CTE. Yang tidak mendukung: MySQL 5.7 dan versi yang lebih lama, di sana kata kunci WITH menghasilkan syntax error, dan alternatifnya adalah derived table (subquery di klausa FROM) atau upgrade ke MySQL 8. Untuk recursive CTE, PostgreSQL, MySQL 8, dan SQLite memakai WITH RECURSIVE, sedangkan SQL Server cukup WITH.
Langkah Selanjutnya
Kamu sekarang sudah pegang seluruh peta CTE: sintaks WITH, alasan keberadaannya, multiple CTE, posisinya dibanding subquery/view/temp table, recursive CTE, sampai pola pipeline filter → agregasi → ranking yang dipakai di dunia kerja. Seperti biasa, yang membedakan paham dan bisa adalah latihan, dan CTE termasuk topik yang cepat melekat begitu kamu menulisnya beberapa kali dengan tanganmu sendiri.
Mulai dari lesson CTE di Hyper Sheets yang latihannya langsung dinilai di browser, lalu lanjutkan ke window functions supaya kombinasi CTE + ranking di walkthrough tadi benar-benar jadi refleks. Dan kalau selama ini kamu lebih sering menyentuh database lewat ORM seperti Eloquent, memahami CTE akan mengubah caramu menulis query kompleks di aplikasi, konteks database di dunia Laravel bisa kamu mulai dari panduan belajar Laravel untuk pemula. Selamat ngoding!