Empat chapter sebelumnya memperkenalkan UNION, UNION ALL, CTE, dan WITH RECURSIVE satu per satu dengan contoh yang memang dirancang membutuhkan fiturnya. Di lapangan, keputusan sehari-hari kita justru lebih sering berbunyi sebaliknya: perlukah query ini dibungkus CTE, atau justru lebih jelas dibiarkan sebagai SELECT polos? CTE adalah alat penata keterbacaan, bukan gaya wajib — dibalik kejelasannya ada biaya baris tambahan, dan dipakai pada tempat yang salah ia hanya memindahkan kebingungan ke nama baru. Chapter penutup section ini merangkum kapan CTE membantu dan kapan tidak, membandingkannya dengan VIEW dan subquery, menjelaskan konsep materialisasi versi ringkas, lalu menuntaskan semuanya dengan pipeline transformasi bertahap dari data mentah sampai laporan pelanggan terbaik.
Kapan CTE Benar-Benar Membantu #
Tiga sinyal kuat menandakan bahwa query layak ditata dengan CTE. Sinyal pertama adalah kedalaman bertingkat: subquery bersarang dua lapis atau lebih terbaca dari dalam ke luar, sementara CTE membaliknya menjadi daftar langkah dari atas ke bawah. Sinyal kedua adalah pengulangan definisi: bila hasil subquery yang sama dibutuhkan di dua tempat, satu CTE menggantikan dua salinan query. Sinyal ketiga adalah rangkaian transformasi: query yang menyaring, lalu mengagregasi, lalu memeringkat berjalan lebih jelas sebagai tahapan bernama. Kita siapkan data uji yang memuat ketiga sinyal itu:
DROP DATABASE IF EXISTS belajar_set;
CREATE DATABASE belajar_set;
USE belajar_set;Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changedCREATE TABLE transaksi (
id INT AUTO_INCREMENT PRIMARY KEY,
tanggal DATE NOT NULL,
pelanggan VARCHAR(50) NOT NULL,
produk VARCHAR(50) NOT NULL,
jumlah INT NOT NULL,
harga DECIMAL(10,2) NOT NULL,
status VARCHAR(10) NOT NULL
);
INSERT INTO transaksi (tanggal, pelanggan, produk, jumlah, harga, status) VALUES
('2026-03-02', 'Budi', 'Kopi Gayo', 2, 55000.00, 'selesai'),
('2026-03-04', 'Siti', 'Teh Melati', 3, 25000.00, 'selesai'),
('2026-03-05', 'Andi', 'Kopi Toraja', 4, 62000.00, 'batal'),
('2026-03-10', 'Budi', 'Gula Aren', 5, 15000.00, 'selesai'),
('2026-03-12', 'Rina', 'Teh Melati', 2, 25000.00, 'selesai'),
('2026-03-18', 'Dewi', 'Kopi Gayo', 3, 55000.00, 'batal'),
('2026-03-22', 'Siti', 'Kopi Toraja', 5, 62000.00, 'selesai'),
('2026-03-25', 'Andi', 'Gula Aren', 6, 15000.00, 'selesai'),
('2026-03-28', 'Tono', 'Teh Melati', 4, 25000.00, 'selesai'),
('2026-03-30', 'Rina', 'Gula Aren', 1, 15000.00, 'batal');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 10 rows affected (0.00 sec)
Records: 10 Duplicates: 0 Warnings: 0Sepuluh transaksi Maret ini memuat tiga berstatus batal yang harus disaring dari laporan, dan lima pelanggan dengan total belanja yang berbeda-beda — bahan yang cukup untuk semua percobaan di chapter ini.
Kapan CTE Justru Menambah Ribut #
Sinyal kebalikannya sama pentingnya: query pendek dengan satu langkah tidak membutuhkan CTE. Hitungan jumlah transaksi selesai per pelanggan sudah sempurna berdiri sendiri:
SELECT pelanggan, COUNT(*) AS jumlah_transaksi
FROM transaksi
WHERE status = 'selesai'
GROUP BY pelanggan
ORDER BY pelanggan;Output yang Dihasilkan:
+-----------+------------------+
| pelanggan | jumlah_transaksi |
+-----------+------------------+
| Andi | 1 |
| Budi | 2 |
| Rina | 1 |
| Siti | 2 |
| Tono | 1 |
+-----------+------------------+
5 rows in set (0.00 sec)Versi yang sama dibungkus CTE menghasilkan keluaran identik dengan baris ekstra:
WITH transaksi_selesai AS (
SELECT pelanggan FROM transaksi WHERE status = 'selesai'
)
SELECT pelanggan, COUNT(*) AS jumlah_transaksi
FROM transaksi_selesai
GROUP BY pelanggan
ORDER BY pelanggan;Output yang Dihasilkan:
+-----------+------------------+
| pelanggan | jumlah_transaksi |
+-----------+------------------+
| Andi | 1 |
| Budi | 2 |
| Rina | 1 |
| Siti | 2 |
| Tono | 1 |
+-----------+------------------+
5 rows in set (0.00 sec)Kedua versi mengembalikan lima baris yang sama, tetapi versi CTE memaksa pembaca melompat ke definisi transaksi_selesai hanya untuk menemukan satu klausa WHERE — abstraksi yang tidak menyembunyikan kerumitan apa pun. Aturan praktisnya: pecah query ke dalam CTE seiring jumlah tahapan logikanya, bukan karena tren. Komposisi berlebihan juga punya biaya pemeliharaan tersendiri; setiap nama tahap adalah kosakata baru yang harus dihafal tim sebelum bisa membaca query dengan lancar.
CTE vs VIEW: Soal Persistensi #
Perbedaan mendasar keduanya ada pada umur dan tempat tinggal definisi. CTE dideklarasikan di dalam statement dan lenyap begitu statement selesai — kita telah membuktikannya dengan ERROR 1146 di chapter CTE. VIEW sebaliknya adalah objek database yang tersimpan permanen dan bisa dipanggil statement mana pun, oleh sesi siapa pun:
CREATE VIEW v_rekap_pelanggan AS
SELECT pelanggan, SUM(jumlah * harga) AS total_belanja
FROM transaksi
WHERE status = 'selesai'
GROUP BY pelanggan;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)View di atas tersimpan di belajar_set sebagai objek bernama v_rekap_pelanggan. Blok SQL berikutnya membuktikan definisinya tetap hidup lintas statement — dan lintas sesi klien, karena tiap blok di materi ini berjalan dalam sesi terpisah:
SELECT * FROM v_rekap_pelanggan
ORDER BY total_belanja DESC, pelanggan
LIMIT 3;Output yang Dihasilkan:
+-----------+---------------+
| pelanggan | total_belanja |
+-----------+---------------+
| Siti | 385000.00 |
| Budi | 185000.00 |
| Tono | 100000.00 |
+-----------+---------------+
3 rows in set (0.00 sec)Perbandingan keduanya secara ringkas:
| Aspek | CTE (WITH) | VIEW |
|---|---|---|
| Tempat definisi | di dalam statement | tersimpan di database |
| Masa berlaku | satu statement saja | sampai di-DROP |
| Bisa dibagi antar query | tidak | ya, termasuk ke aplikasi lain |
| Cocok untuk | logika khusus satu laporan | logika standar yang dipakai berulang |
Pilih VIEW saat logika rekap ini akan dipanggil banyak laporan atau banyak aplikasi; pilih CTE saat logikanya memang milik satu statement itu saja. Section khusus tentang VIEW menunggu di bagian akhir course ini, jadi kita cukup membawa intinya ke depan.
CTE vs Subquery #
Subquery dan CTE menyampaikan mesin yang sama — keduanya dieksekusi sebagai bagian dari query — sehingga pilihan di antara keduanya murni soal keterbacaan dan pengulangan. Subquery skalar pendek di klausa WHERE, misalnya (SELECT AVG(jumlah) FROM transaksi), justru paling jelas dibiarkan menempel di tempat pemakaiannya. CTE mengambil alih saat definisi itu panjang, dipakai lebih dari sekali, atau menjadi salah satu tahapan pipeline. Anggap saja CTE sebagai alat tata letak: ia tidak mengubah hasil, hanya mengubah urutan membaca dari “bentuk sarang burung” menjadi “daftar langkah bernomor”.
Pipeline Transformasi Bertahap #
Bentuk pemakaian CTE paling matang adalah pipeline: data mentah mengalir melewati tahapan-tahapan bernama sebelum tampil sebagai laporan. Kita bangun laporan tiga pelanggan terbaik Maret dari tabel transaksi mentah:
WITH bersih AS (
SELECT pelanggan, jumlah, harga
FROM transaksi
WHERE status = 'selesai'
),
rekap AS (
SELECT pelanggan,
COUNT(*) AS jumlah_transaksi,
SUM(jumlah * harga) AS total_belanja
FROM bersih
GROUP BY pelanggan
)
SELECT pelanggan, jumlah_transaksi, total_belanja
FROM rekap
ORDER BY total_belanja DESC, pelanggan
LIMIT 3;Output yang Dihasilkan:
+-----------+------------------+---------------+
| pelanggan | jumlah_transaksi | total_belanja |
+-----------+------------------+---------------+
| Siti | 2 | 385000.00 |
| Budi | 2 | 185000.00 |
| Tono | 1 | 100000.00 |
+-----------+------------------+---------------+
3 rows in set (0.00 sec)Mari kita ikuti aliran datanya tahap demi tahap: CTE pertama bersih mengambil transaksi mentah sepuluh baris dan menyaring tiga transaksi batal, menyisakan kolom yang diperlukan saja. CTE kedua rekap membaca bersih tujuh baris lalu mengagregasi per pelanggan menjadi lima baris berisi jumlah transaksi dan total belanja. Query utama menutup pipeline dengan memeringkat lima baris itu berdasarkan total_belanja dan memotong tiga teratas: Siti (385.000), Budi (185.000), Tono (100.000). Setiap tahap bisa diuji sendiri dengan mengganti query utama menjadi SELECT * FROM bersih atau SELECT * FROM rekap — keunggulan debugging yang tidak dimiliki subquery raksasa satu blok.
Materialisasi: Dikerjakan Sekali atau Digabung #
Satu pertanyaan teknis kerap muncul: apakah CTE yang dipanggil dua kali dieksekusi dua kali? Jawabannya berada di tangan optimizer MySQL, bukan di tangan kita. Untuk derived table dan CTE, MySQL dapat memilih mengeksekusi definisi sekali lalu menyimpan hasilnya sementara di memori atau tabel internal — disebut dimaterialisasi — atau menggabungkan definisi langsung ke query utama agar disederhanakan. CTE yang dirujuk berulang umumnya dimaterialisasi agar pekerjaannya tidak diulang; CTE yang dirujuk sekali sering digabungkan karena cukup murah disederhanakan. Kita tidak perlu mengatur pilihan itu sehari-hari — cukup tahu bahwa menulis CTE dua rujukan tidak otomatis berarti eksekusi ganda. Programmer yang ingin mengarahkan optimizer secara eksplisit dapat memakai optimizer hints, pembahasan lanjutan di section query optimization.
Batasan dan Kebiasaan Penulisan #
Dua kebiasaan penulisan menutup chapter ini. Pertama, nama kolom CTE bisa dideklarasikan eksplisit di samping namanya — daftar nama dalam tanda kurung setelah nama CTE:
WITH rekap (pelanggan, total_unit) AS (
SELECT pelanggan, SUM(jumlah)
FROM transaksi
WHERE status = 'selesai'
GROUP BY pelanggan
)
SELECT * FROM rekap
ORDER BY total_unit DESC, pelanggan
LIMIT 3;Output yang Dihasilkan:
+-----------+------------+
| pelanggan | total_unit |
+-----------+------------+
| Siti | 8 |
| Budi | 7 |
| Andi | 6 |
+-----------+------------+
3 rows in set (0.00 sec)Daftar (pelanggan, total_unit) menamai kolom hasil sejak deklarasi, sehingga query utama tidak perlu mengandalkan alias dari dalam definisi. Jumlah nama di daftar harus sama dengan jumlah kolom hasil subquery. Kedua, setiap CTE dalam satu statement harus bernama unik — penulisan ulang nama yang sama ditolak keras:
-- @error
WITH a AS (SELECT 1 AS x), a AS (SELECT 2 AS y) SELECT * FROM a;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1066 (42000): Not unique table/alias: 'a'
ByeServer menolak dengan ERROR 1066 karena dua CTE yang bernama sama di satu statement akan saling bertabrakan saat dipanggil. Batasan besar lainnya sudah kita buktikan di chapter sebelumnya: seluruh rangkaian WITH hanya berlaku untuk satu statement — tidak ada cara memperpanjang umurnya selain menjadikannya VIEW atau tabel sementara.
Membersihkan Database Latihan #
Rangkaian latihan selesai — view dijatuhkan lebih dulu, lalu database latihannya:
DROP VIEW v_rekap_pelanggan;
DROP DATABASE belajar_set;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 1 row affected (0.00 sec)Referensi #
- WITH (Common Table Expressions) — MySQL 8.4 Reference Manual
- Views — MySQL 8.4 Reference Manual
- Derived Table Optimization — MySQL 8.4 Reference Manual
Section set operasi dan CTE resmi kita tutup: hasil query bisa digabung vertikal dengan UNION dan UNION ALL, query panjang dirapikan dengan WITH berantai, deret dan hierarki dibangkitkan lewat WITH RECURSIVE, dan keputusan memakai CTE kini berpijak pada sinyal yang jelas. Semua bekal ini adalah bekal pembacaan dan penulisan query; rancangan struktur datanya sendiri adalah dunia baru. Section berikutnya pindah ke relational database design — merancang entitas, relasi antartabel, dan normalisasi supaya skema yang menampung query-query kita kokoh sejak dari fondasi.