CTE rekursif adalah common table expression yang bagian terakhirnya memanggil dirinya sendiri, sehingga sebuah query dapat menelusuri hubungan berulang seperti hierarki kategori dan silsilah organisasi tanpa menulis perulangan di luar SQL. Topik ini membahas struktur anggota jangkar dan anggota rekursif, penelusuran hierarki kategori perpustakaan dari akar ke daun, agregasi yang mencakup seluruh subtree, serta batas kedalaman yang diatur server. Setelah mengikuti topik ini, data bertingkat dapat dikonversi menjadi daftar lurus yang siap ditampilkan atau dirangkum.
Struktur CTE rekursif#
CTE rekursif dideklarasikan dengan WITH RECURSIVE dan terdiri atas dua bagian
yang dihubungkan UNION ALL. Bagian pertama disebut jangkar (anchor), yaitu
query yang menghasilkan baris-baris awal. Bagian kedua disebut anggota rekursif
(recursive member), yaitu query yang merujuk nama CTE-nya sendiri dan
menghasilkan baris berikutnya dari baris sebelumnya.
-- tidak-dijalankan
WITH RECURSIVE nama_cte AS (
SELECT ... -- jangkar: baris awal, biasanya baris akar
UNION ALL
SELECT ... -- anggota rekursif: turunan dari baris sebelumnya
)
SELECT ... FROM nama_cte;Server mengeksekusinya dalam putaran. Putaran pertama menjalankan jangkar dan menyimpan hasilnya. Putaran berikutnya menjalankan anggota rekursif dengan memandang seluruh baris putaran sebelumnya sebagai tabel bernama CTE itu. Eksekusi berhenti ketika anggota rekursif tidak lagi menghasilkan baris baru. Setiap baris yang pernah dihasilkan tetap bertahan, sehingga hasil akhirnya adalah gabungan seluruh putaran.
Hierarki kategori perpustakaan#
Skema contoh memuat tabel kategori yang merujuk dirinya sendiri melalui kolom
parent_id, sebuah pola yang lazim untuk data hierarki. Kategori tanpa
parent_id berperan sebagai akar, sedangkan kategori lain menggantung pada
kategori di atasnya. Diagram berikut memperlihatkan hierarki yang dipakai
sepanjang topik ini beserta tingkatnya.
Jalankan persiapan berikut untuk membangun basis data contoh dari nol.
-- tidak-dijalankan
-- Skrip persiapan topik CTE rekursif: hierarki kategori perpustakaan
DROP DATABASE IF EXISTS perpustakaan;
CREATE DATABASE perpustakaan;
USE perpustakaan;
CREATE TABLE kategori (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
parent_id INT NULL,
FOREIGN KEY (parent_id) REFERENCES kategori(id)
);
CREATE TABLE buku (
id INT AUTO_INCREMENT PRIMARY KEY,
judul VARCHAR(200) NOT NULL,
kategori_id INT NOT NULL,
FOREIGN KEY (kategori_id) REFERENCES kategori(id)
);
INSERT INTO kategori (id, nama, parent_id) VALUES
(1, 'Fiksi', NULL),
(2, 'Nonfiksi', NULL),
(3, 'Fiksi Sejarah', 1),
(4, 'Fiksi Sains', 1),
(5, 'Fiksi Detektif', 1),
(6, 'Sains Alam', 2),
(7, 'Astronomi', 6),
(8, 'Biologi', 6),
(9, 'Teknologi', 2);
INSERT INTO buku (judul, kategori_id) VALUES
('Kronik Kerajaan Bahari', 3),
('Kapal dan Jalur Rempah', 3),
('Planet Tetangga Kita', 7),
('Peta Langit Malam', 7),
('Serangga Kota Besar', 8),
('Kode Rahasia Perpustakaan', 5),
('Teka-Teki Jam Dinding', 5),
('Misteri Wisma Tua', 5),
('Robot Rumah Tangga', 9),
('Gravitasi untuk Pemula', 4),
('Kebun Obat Keluarga', 6);# membuat ulang basis data perpustakaan dari nol
mysql -u root < persiapan-cte-rekursif.sqlPenelusuran dari akar ke daun#
Jangkar mengambil kategori akar, yaitu baris dengan parent_id bernilai NULL,
lalu melengkapinya dengan kolom tingkat bernilai satu. Anggota rekursif
mencari anak dari setiap baris pada putaran sebelumnya dan menaikkan nilai
tingkat. Kolom jalur dirakit dengan CONCAT sehingga tiap baris membawa
rangkaian nama dari akar sampai kategori itu.
# menelusuri seluruh kategori dari akar ke daun
mysql -t -u root perpustakaan <<'SQL'
WITH RECURSIVE turun AS (
SELECT id, nama, parent_id, 1 AS tingkat,
CAST(nama AS CHAR(200)) AS jalur
FROM kategori
WHERE parent_id IS NULL
UNION ALL
SELECT k.id, k.nama, k.parent_id, t.tingkat + 1,
CONCAT(t.jalur, ' / ', k.nama)
FROM kategori AS k
JOIN turun AS t ON k.parent_id = t.id
)
SELECT id, nama, tingkat, jalur
FROM turun
ORDER BY id;
SQL+------+----------------+---------+-----------------------------------+
| id | nama | tingkat | jalur |
+------+----------------+---------+-----------------------------------+
| 1 | Fiksi | 1 | Fiksi |
| 2 | Nonfiksi | 1 | Nonfiksi |
| 3 | Fiksi Sejarah | 2 | Fiksi / Fiksi Sejarah |
| 4 | Fiksi Sains | 2 | Fiksi / Fiksi Sains |
| 5 | Fiksi Detektif | 2 | Fiksi / Fiksi Detektif |
| 6 | Sains Alam | 2 | Nonfiksi / Sains Alam |
| 7 | Astronomi | 3 | Nonfiksi / Sains Alam / Astronomi |
| 8 | Biologi | 3 | Nonfiksi / Sains Alam / Biologi |
| 9 | Teknologi | 2 | Nonfiksi / Teknologi |
+------+----------------+---------+-----------------------------------+Kolom tingkat menjelaskan kedalaman setiap kategori, sedangkan jalur
menyediakan jejak dari akar yang siap ditampilkan pada menu atau breadcrumb.
CAST(nama AS CHAR(200)) pada jangkar menetapkan tipe kolom jalur agar
cukup lebar untuk gabungan nama sepanjang hierarki.
Agregasi lintas subtree#
Pertanyaan yang lebih sulit adalah menghitung buku per kategori termasuk seluruh subkategori. CTE rekursif menjawabnya dengan membangun tabel pasangan keturunan. Jangkar memasangkan setiap kategori dengan dirinya sendiri, lalu anggota rekursif menggandakan pasangan itu ke setiap anak di bawahnya. Hasil akhirnya memuat pasangan dari setiap kategori ke seluruh keturunannya, dan pernyataan utama tinggal menghitung buku per pasangan.
# jumlah buku per kategori termasuk seluruh subkategori
mysql -t -u root perpustakaan <<'SQL'
WITH RECURSIVE keturunan AS (
SELECT id AS akar_id, id AS keturunan_id
FROM kategori
UNION ALL
SELECT t.akar_id, k.id
FROM keturunan AS t
JOIN kategori AS k ON k.parent_id = t.keturunan_id
)
SELECT ka.nama AS kategori, COUNT(b.id) AS jumlah_buku
FROM keturunan AS t
JOIN kategori AS ka ON ka.id = t.akar_id
LEFT JOIN buku AS b ON b.kategori_id = t.keturunan_id
GROUP BY t.akar_id, ka.nama
ORDER BY jumlah_buku DESC, ka.nama;
SQL+----------------+-------------+
| kategori | jumlah_buku |
+----------------+-------------+
| Fiksi | 6 |
| Nonfiksi | 5 |
| Sains Alam | 4 |
| Fiksi Detektif | 3 |
| Astronomi | 2 |
| Fiksi Sejarah | 2 |
| Biologi | 1 |
| Fiksi Sains | 1 |
| Teknologi | 1 |
+----------------+-------------+Angka untuk Sains Alam memperlihatkan kekuatan pola ini. Buku yang tercatat
langsung pada Sains Alam hanya satu judul, tetapi hasilnya empat karena
Astronomi menyumbang dua judul dan Biologi satu judul. Hierarki telah
diratakan menjadi pasangan sederhana, sehingga agregasi biasa dapat
mengikutinya tanpa trik tambahan.
Batas kedalaman rekursi#
Server melindungi dirinya dari rekursi yang tidak berhenti dengan variabel
sesi cte_max_recursion_depth, yang bawaannya bernilai 1000 putaran. Contoh
berikut membangun tabel rantai berupa deretan 1200 simpul, dan setiap
simpul menggantung pada simpul sebelumnya. Pembangunannya sendiri memakai CTE
rekursif dan melewati batas tersebut.
# membangun rantai 1200 simpul melebihi batas bawaan
mysql -u root perpustakaan <<'SQL'
CREATE TABLE rantai (id INT PRIMARY KEY, induk INT NULL);
INSERT INTO rantai
WITH RECURSIVE urutan AS (
SELECT 1 AS id
UNION ALL
SELECT id + 1 FROM urutan WHERE id < 1200
)
SELECT id, IF(id = 1, NULL, id - 1) FROM urutan;
SQLERROR 3636 (HY000) at line 2: Recursive query aborted after 1001 iterations. Try increasing @@cte_max_recursion_depth to a larger value.Error 3636 muncul setelah putaran seribu satu, dan pernyataan yang gagal tidak
menyisakan baris apa pun pada tabel rantai. Penaikan batas dilakukan per
sesi dengan SET SESSION, sehingga sesi lain tidak terpengaruh. Pada sesi
yang sama, persiapan diulang dan penelusuran penuh dijalankan.
# menaikkan batas kedalaman CTE untuk sesi ini
mysql -t -u root perpustakaan <<'SQL'
SET SESSION cte_max_recursion_depth = 2000;
INSERT INTO rantai
WITH RECURSIVE urutan AS (
SELECT 1 AS id
UNION ALL
SELECT id + 1 FROM urutan WHERE id < 1200
)
SELECT id, IF(id = 1, NULL, id - 1) FROM urutan;
WITH RECURSIVE turun AS (
SELECT id FROM rantai WHERE induk IS NULL
UNION ALL
SELECT r.id FROM rantai AS r JOIN turun AS t ON r.induk = t.id
)
SELECT COUNT(*) AS simpul_dikunjungi FROM turun;
SQL+-------------------+
| simpul_dikunjungi |
+-------------------+
| 1200 |
+-------------------+Kesalahan umum#
Kata kunci RECURSIVE wajib ditulis ketika CTE memanggil dirinya sendiri.
Tanpa kata itu, server menilai nama CTE di dalam anggota rekursif sebagai
tabel biasa yang memang tidak ada.
# tanpa kata kunci RECURSIVE, anggota rekursif dinilai sebagai tabel
mysql -u root perpustakaan -e "WITH turun AS (SELECT id FROM kategori WHERE parent_id IS NULL UNION ALL SELECT k.id FROM turun t JOIN kategori k ON k.parent_id = t.id) SELECT COUNT(*) AS jumlah FROM turun;"ERROR 1146 (42S02) at line 1: Table 'perpustakaan.turun' doesn't existPerhatikan pula pilihan antara UNION ALL dan UNION. Anggota rekursif
umumnya memakai UNION ALL karena tidak ada baris duplikat pada penelusuran
pohon, sedangkan UNION menyaring duplikat dengan biaya tambahan pada setiap
putaran. Terakhir, kolom teks hasil rakitan seperti jalur sebaiknya diberi
tipe lewat CAST pada jangkar. Tanpa CAST, tipe kolom mengikuti kolom asal
yang lebih pendek, dan nilai panjang dapat terpotong atau ditolak.
Ringkasan#
- CTE rekursif terdiri atas jangkar yang menghasilkan baris awal dan anggota
rekursif yang memanggil CTE-nya sendiri melalui
UNION ALL. - Eksekusi berlangsung dalam putaran hingga anggota rekursif tidak lagi menghasilkan baris, dan seluruh hasil putaran digabungkan.
- Pola pasangan akar-keturunan meratakan hierarki, sehingga agregasi seperti
jumlah buku per subtree menjadi
GROUP BYbiasa. - Batas kedalaman diatur oleh
cte_max_recursion_depth, yang dinaikkan per sesi denganSET SESSIONbila hierarki melebihi seribu tingkat.
Referensi#
- Recursive Common Table Expressions — MySQL 8.0 Reference Manual — Semantik jangkar, anggota rekursif, dan batas kedalaman CTE rekursif.
- Restrictions on Common Table Expressions — MySQL 8.0 Reference Manual
— Larangan yang berlaku pada anggota rekursif, termasuk
ORDER BYdanLIMIT.