Menu

CTE Rekursif SQLite: WITH RECURSIVE untuk Hierarki

Cara kerja CTE rekursif di SQLite: bentuk anchor dan bagian rekursif, menelusuri pohon induk dan anak, membuat deret, dan menghindari perulangan tanpa akhir.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Rekursi di SQL Terdengar Aneh, Sampai Kamu Melihatnya

Kebanyakan query mengembalikan baris dari data yang sudah ada. CTE rekursif berbeda: CTE ini membangun baris dengan memasukkan output-nya sendiri kembali sebagai input, selangkah demi selangkah, sampai tidak ada lagi yang baru untuk ditambahkan. Dengan cara itulah kamu menelusuri pohon yang kedalamannya tidak diketahui, atau membuat angka 1 sampai 100 tanpa tabel angka.

Bentuknya selalu sama:

WITH RECURSIVE name(columns) AS (
    -- anchor: the starting rows
    SELECT ...
    UNION ALL
    -- recursive: rows derived from the previous step
    SELECT ... FROM name WHERE ...
)
SELECT * FROM name;

Anchor di atas, UNION ALL, lalu query rekursif di bawahnya. SQLite menjalankan anchor sekali, lalu terus menjalankan bagian rekursifnya, setiap kali memakai baris yang dihasilkan di putaran terakhir, sampai tidak ada baris baru yang dihasilkan. Lalu SQLite berhenti.

Menghitung dari 1 sampai 10

CTE rekursif yang paling sederhana membuat sebuah deret. Tidak butuh tabel:

Telusuri langkahnya:

  1. Anchor menghasilkan satu baris: n = 1.
  2. Langkah rekursif mengambil baris itu, menghitung n + 1 = 2, dan 2 < 10 bernilai benar, jadi barisnya disimpan.
  3. Iterasi berikutnya mengambil n = 2 dan menghasilkan n = 3. Dan seterusnya.
  4. Saat n mencapai 10, 10 < 10 bernilai salah, langkah rekursif tidak mengembalikan baris, dan SQLite berhenti.

WHERE n < 10 adalah kondisi berhentinya. Tanpanya, query itu berjalan selamanya.

Membuat Deret Tanggal

Idenya sama, dan berguna di laporan sungguhan: mengisi setiap hari dalam suatu rentang, termasuk hari-hari yang tidak ada kejadiannya:

Biasanya kamu akan melakukan LEFT JOIN deret ini dengan tabel event agar hari tanpa event terhitung dengan benar. GROUP BY date biasa sama sekali melewati hari yang kosong; deret tanggal memberimu satu baris untuk setiap hari, apa pun yang terjadi.

Menelusuri Pohon Induk dan Anak

Kasus pemakaian yang klasik. Ini tabel karyawan yang setiap barisnya menunjuk ke manajernya:

Anchor memilih akarnya (orang yang tidak punya manajer). Langkah rekursif melakukan join tabel karyawan kembali dengan CTE, mencari semua orang yang manager_id-nya cocok dengan id yang sudah ada di CTE. Setiap iterasi turun satu level lebih dalam. depth hanyalah penghitung yang kita tambahkan untuk membuat indentasi pada output.

Cara ini berfungsi untuk pohon sedalam apa pun. Dua level, sepuluh level, query-nya tidak berubah.

Mencari Semua Leluhur dari Baris Tertentu

Balik arahnya. Alih-alih menelusuri turun dari akar, telusuri naik dari karyawan tertentu untuk menemukan seluruh rantai manajernya:

Anchor-nya adalah karyawan awal. Setiap langkah rekursif melompat ke induknya. SQLite berhenti saat mencapai akar: manager_id IS NULL, sehingga join tidak menemukan apa pun.

Pola ini berguna untuk breadcrumb, komentar berutas, jalur kategori, dan di mana pun kamu perlu "menelusuri naik sampai puncak."

Kondisi Berhenti dan Perulangan Tanpa Akhir

Bug yang paling umum adalah lupa menulis kondisi berhenti, atau menulis kondisi yang tidak pernah terpenuhi. Bandingkan:

-- Runs forever:
WITH RECURSIVE bad(n) AS (
    SELECT 1
    UNION ALL
    SELECT n + 1 FROM bad
)
SELECT n FROM bad;

Tidak ada klausa WHERE yang pada suatu saat mengembalikan nol baris. SQLite akan dengan senang hati mencoba menghitung sampai tak terhingga.

Dua kebiasaan untuk berjaga-jaga:

  1. Selalu sertakan klausa WHERE di bagian rekursif yang membatasi pertumbuhannya.
  2. Tambahkan LIMIT di SELECT luar sebagai pengaman selama kamu masih mengembangkan query-nya: jika kamu salah soal kondisi berhentinya, query-nya tetap berhenti.

CTE-nya sendiri tidak dibatasi, tapi LIMIT 5 menghentikan query luarnya lebih awal. SQLite cukup cerdas untuk tidak terus merekursi melewati apa yang dibutuhkan LIMIT. Berguna untuk eksplorasi, tapi bukan pengganti kondisi berhenti yang sesungguhnya di kode produksi.

Siklus di Graf

Pohon tidak bisa punya siklus. Graf umum bisa, dan CTE rekursif yang naif akan berputar selamanya jika datanya punya siklus. Solusinya adalah mencatat jalur yang sudah ditelusuri sejauh ini dan menolak mengunjungi node yang sama lagi:

path adalah string berisi node-node yang sudah dikunjungi, dipisahkan koma. Sebelum menambahkan node baru, klausa WHERE mengecek bahwa node itu belum ada di sana. Tanpa penjaga itu, siklus 1 → 2 → 3 → 1 akan berputar selamanya.

Tidak ada "himpunan node yang sudah dikunjungi" bawaan di SQL. Kamu membuatnya sendiri, biasanya sebagai string atau dengan join terhadap isi CTE sejauh ini.

CTE Rekursif vs Self Join

Jika kamu hanya butuh satu atau dua level, self join lebih sederhana dan lebih cepat:

Query itu menjawab "siapa manajer langsung setiap orang." Tapi jika kamu butuh "semua orang yang melapor ke atas sampai Ada, sedalam apa pun", yang kedalamannya tidak diketahui, hanya CTE rekursif yang bisa menanganinya dengan rapi. Pilih alat yang sesuai dengan kedalaman yang kamu butuhkan:

  • Kedalaman tetap dan kecil: self join, mungkin dua atau tiga kali.
  • Kedalaman tidak diketahui atau sembarang: WITH RECURSIVE.

Model Berpikirnya

CTE rekursif adalah sebuah perulangan yang ditulis secara deklaratif:

  • Anchor adalah nilai awal perulangannya.
  • Query rekursif adalah badan perulangannya, yang menghasilkan kelompok baris berikutnya dari baris saat ini.
  • Kondisi berhenti adalah pengujian untuk keluar dari perulangan: saat mengembalikan nol baris, perulangannya selesai.
  • UNION ALL mengumpulkan semuanya ke dalam hasil akhir.

Begitu gambaran ini tertanam di kepalamu, sintaksnya tidak lagi terasa aneh. Kamu sedang menulis for loop di SQL.

Selanjutnya: Index

CTE rekursif menelusuri banyak baris, dan join di dalam langkah rekursifnya berjalan di setiap iterasi. Jika kolom join-nya tidak punya index, performanya anjlok dengan cepat. Index adalah bab berikutnya, dan manager_id adalah jenis kolom yang sangat diuntungkan oleh index.

Pertanyaan yang sering diajukan

Apa itu CTE rekursif di SQLite?

CTE rekursif adalah query WITH RECURSIVE yang membangun hasilnya dengan merujuk dirinya sendiri berulang kali. Query ini punya dua bagian yang digabung dengan UNION ALL: query anchor yang menghasilkan baris awal, dan query rekursif yang menghasilkan baris baru dari langkah sebelumnya. SQLite terus menjalankan bagian rekursifnya sampai tidak ada baris baru yang dihasilkan.

Kapan sebaiknya memakai WITH RECURSIVE di SQLite?

Pakai saat kamu perlu menelusuri pohon atau graf (karyawan dan manajernya, kategori dan subkategori, komentar berutas) atau membuat deret (setiap tanggal dalam suatu rentang, angka 1 sampai 100). Join biasa hanya menangani satu atau dua level; CTE rekursif menangani kedalaman berapa pun tanpa perlu tahu kedalamannya lebih dulu.

Bagaimana cara menghindari perulangan tanpa akhir di CTE rekursif SQLite?

Pastikan query rekursifnya punya kondisi berhenti, yaitu klausa WHERE yang pada akhirnya mengembalikan nol baris, atau penghitung yang kamu batasi. Untuk graf yang punya siklus, catat jalur yang sudah dikunjungi di sebuah kolom dan kecualikan baris yang sudah ada di jalur itu. Sebagai pengaman, tambahkan LIMIT di query luar agar rekursi yang lepas kendali tidak memenuhi memori.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai