Asumsikan Anda memiliki tabel datar yang menyimpan hierarki pohon yang dipesan:
Id Name ParentId Order
1 'Node 1' 0 10
2 'Node 1.1' 1 10
3 'Node 2' 0 20
4 'Node 1.1.1' 2 10
5 'Node 2.1' 3 10
6 'Node 1.2' 1 20
Berikut diagram, di mana kita miliki [id] Name
. Root node 0 bersifat fiksi.
[0] ROOT / \ [1] Node 1 [3] Node 2 / \ \ [2] Node 1.1 [6] Node 1.2 [5] Node 2.1 / [4] Node 1.1.1
Pendekatan minimalis apa yang akan Anda gunakan untuk menampilkan itu ke HTML (atau teks, dalam hal ini) sebagai pohon dengan indentasi yang disusun dengan benar?
Asumsikan lebih lanjut Anda hanya memiliki struktur data dasar (array dan hashmaps), tidak ada objek mewah dengan referensi orang tua / anak, tidak ada ORM, tidak ada kerangka kerja, hanya dua tangan Anda. Tabel diwakili sebagai set hasil, yang dapat diakses secara acak.
Kode semu atau bahasa Inggris biasa tidak apa-apa, ini murni pertanyaan konseptual.
Pertanyaan bonus: Apakah ada cara yang secara fundamental lebih baik untuk menyimpan struktur pohon seperti ini di RDBMS?
EDIT DAN TAMBAHAN
Untuk menjawab pertanyaan salah satu komentator ( Mark Bessey ): Simpul root tidak diperlukan, karena tidak akan pernah ditampilkan. ParentId = 0 adalah konvensi untuk menyatakan "ini adalah level teratas". Kolom Pesanan menentukan bagaimana simpul dengan induk yang sama akan diurutkan.
"Set hasil" yang saya bicarakan dapat digambarkan sebagai array hashmaps (untuk tetap dalam terminologi itu). Untuk contoh saya seharusnya sudah ada di sana. Beberapa jawaban bekerja lebih keras dan membangunnya terlebih dahulu, tapi tidak apa-apa.
Pohonnya bisa sedalam mungkin. Setiap simpul dapat memiliki N anak. Namun, saya sebenarnya tidak memiliki pohon "jutaan entri".
Jangan salah pilih penamaan simpul ('Node 1.1.1') untuk sesuatu yang bisa diandalkan. Node dapat juga disebut 'Frank' atau 'Bob', tidak ada struktur penamaan yang tersirat, ini hanya untuk membuatnya dapat dibaca.
Saya telah memposting solusi saya sendiri sehingga kalian bisa menariknya berkeping-keping.
Jawaban:
Sekarang MySQL 8.0 mendukung kueri rekursif , kita dapat mengatakan bahwa semua database SQL populer mendukung kueri rekursif dalam sintaksis standar.
Saya menguji permintaan rekursif di MySQL 8.0 di presentasi saya Recursive Query Throwdown pada 2017.
Di bawah ini adalah jawaban asli saya dari 2008:
Ada beberapa cara untuk menyimpan data terstruktur pohon dalam database relasional. Apa yang Anda perlihatkan dalam contoh Anda menggunakan dua metode:
Solusi lain disebut Nested Sets , dan dapat disimpan dalam tabel yang sama juga. Baca " Pohon dan Hirarki dalam SQL untuk Smarties " oleh Joe Celko untuk informasi lebih lanjut tentang desain ini.
Saya biasanya lebih suka desain yang disebut Tabel Penutupan (alias "Hubungan Adjacency") untuk menyimpan data terstruktur pohon. Membutuhkan tabel lain, tetapi kemudian permintaan pohon cukup mudah.
Saya membahas Tabel Penutupan dalam Model presentasi saya untuk Data Hirarki dengan SQL dan PHP dan dalam buku saya SQL Antipatterns: Menghindari Jebakan Pemrograman Basis Data .
Simpan semua jalur di Tabel Penutupan, di mana ada keturunan langsung dari satu simpul ke simpul lainnya. Sertakan baris untuk setiap node untuk referensi itu sendiri. Misalnya, menggunakan kumpulan data yang Anda tunjukkan dalam pertanyaan Anda:
Sekarang Anda bisa mendapatkan pohon mulai dari simpul 1 seperti ini:
Output (dalam klien MySQL) terlihat seperti berikut:
Dengan kata lain, simpul 3 dan 5 dikecualikan, karena mereka merupakan bagian dari hierarki yang terpisah, bukan turun dari simpul 1.
Re: komentar dari e-satis tentang anak langsung (atau orang tua langsung). Anda dapat menambahkan
path_length
kolom " " keClosureTable
untuk membuatnya lebih mudah untuk meminta secara khusus untuk anak atau orang tua langsung (atau jarak lainnya).Kemudian Anda dapat menambahkan istilah dalam pencarian Anda untuk menanyakan langsung anak-anak dari simpul yang diberikan. Ini adalah keturunan yang
path_length
1.Komentar ulang dari @ashraf: "Bagaimana kalau menyortir seluruh pohon [dengan nama]?"
Berikut ini contoh kueri untuk mengembalikan semua node yang merupakan turunan dari simpul 1, bergabung dengan mereka ke FlatTable yang berisi atribut simpul lainnya seperti
name
, dan urutkan berdasarkan namanya.Komentar ulang dari @Nate:
Seorang pengguna menyarankan suntingan hari ini. SO moderator menyetujui hasil edit, tetapi saya membalikkannya.
Hasil edit menyarankan agar ORDER BY dalam kueri terakhir di atas seharusnya
ORDER BY b.path_length, f.name
, mungkin untuk memastikan pemesanan sesuai dengan hierarki. Tetapi ini tidak berhasil, karena akan memesan "Node 1.1.1" setelah "Node 1.2".Jika Anda ingin pemesanan mencocokkan hierarki dengan cara yang masuk akal, itu mungkin, tetapi tidak hanya dengan memesan berdasarkan panjang jalur. Sebagai contoh, lihat jawaban saya ke database hierarkis Tabel Penutupan MySQL - Cara mengeluarkan informasi dalam urutan yang benar .
sumber
parent_id
) hanya memiliki satu baris untuk mengekspresikan setiap hubungan orangtua-anak, tetapi Tabel Penutupan memiliki banyak baris.Jika Anda menggunakan kumpulan bersarang (kadang-kadang disebut sebagai Modifikasi Traversal Pohon Pra-pemesanan yang Dimodifikasi), Anda dapat mengekstraksi seluruh struktur pohon atau setiap subtree di dalamnya dalam urutan pohon dengan satu permintaan, dengan biaya memasukkan lebih mahal, karena Anda perlu mengelola kolom yang menggambarkan jalur berurutan melalui struktur pohon Anda.
Untuk django-mptt , saya menggunakan struktur seperti ini:
Yang menggambarkan pohon yang terlihat seperti ini (dengan
id
mewakili setiap item):Atau, sebagai set diagram bersarang yang membuatnya lebih jelas bagaimana
lft
danrght
nilai - nilai bekerja:Seperti yang Anda lihat, untuk mendapatkan seluruh subtree untuk node yang diberikan, dalam urutan pohon, Anda cukup memilih semua baris yang memiliki
lft
danrght
nilai - nilai antaralft
danrght
nilai - nilai. Ini juga mudah untuk mengambil pohon leluhur untuk simpul yang diberikan.The
level
kolom sedikit denormalisation untuk kenyamanan lebih dari apa pun dantree_id
kolom memungkinkan Anda untuk me-restartlft
danrght
penomoran untuk setiap node tingkat atas, yang mengurangi jumlah kolom dipengaruhi oleh sisipan, bergerak dan penghapusan, sebagailft
danrght
kolom harus disesuaikan sesuai ketika operasi ini dilakukan untuk membuat atau menutup celah. Saya membuat beberapa catatan pengembangan pada saat saya mencoba membungkus kepala saya di sekitar pertanyaan yang diperlukan untuk setiap operasi.Dalam hal benar-benar bekerja dengan data ini untuk menampilkan pohon, saya membuat
tree_item_iterator
fungsi utilitas yang, untuk setiap node, harus memberi Anda informasi yang cukup untuk menghasilkan tampilan apa pun yang Anda inginkan.Info lebih lanjut tentang MPTT:
sumber
lft
danrght
untuk nama kolom, maksud saya berapa banyak karakter yang tidak perlu kita ketik? satu?!Ini pertanyaan yang cukup lama, tetapi karena punya banyak pandangan saya pikir itu layak untuk menyajikan alternatif, dan menurut saya solusi yang sangat elegan.
Untuk membaca struktur pohon, Anda dapat menggunakan Common Table Expressions (CTE) rekursif . Ini memberikan kemungkinan untuk mengambil seluruh struktur pohon sekaligus, memiliki informasi tentang tingkat simpul, simpul induknya dan urutan dalam anak-anak dari simpul induk.
Biarkan saya menunjukkan kepada Anda bagaimana ini akan bekerja di PostgreSQL 9.1.
Buat struktur
Tulis kueri
Inilah hasilnya:
Simpul pohon disusun berdasarkan tingkat kedalaman. Pada hasil akhir, kami akan mempresentasikannya di baris berikutnya.
Untuk setiap level, mereka diurutkan oleh parent_id dan node_order di dalam parent. Ini memberitahu kita bagaimana menyajikannya dalam simpul tautan keluaran ke induk dalam urutan ini.
Memiliki struktur seperti itu tidak akan sulit untuk membuat presentasi yang sangat bagus dalam HTML.
CTE rekursif tersedia dalam PostgreSQL, IBM DB2, MS SQL Server dan Oracle .
Jika Anda ingin membaca lebih lanjut tentang query SQL rekursif, Anda dapat memeriksa dokumentasi DBMS favorit Anda atau membaca dua artikel saya yang membahas topik ini:
sumber
Pada Oracle 9i, Anda dapat menggunakan CONNECT BY.
Pada SQL Server 2005, Anda dapat menggunakan ekspresi tabel rekursif umum (CTE).
Keduanya akan menampilkan hasil berikut.
sumber
Jawaban Bill sangat baik sekali, jawaban ini menambahkan beberapa hal yang membuat saya berharap SO didukung jawaban berulir.
Pokoknya saya ingin mendukung struktur pohon dan properti Order. Saya menyertakan satu properti di setiap Node yang dipanggil
leftSibling
yang melakukan hal yang samaOrder
dimaksudkan untuk dilakukan dalam pertanyaan asli (mempertahankan urutan kiri-ke-kanan).Lebih detail dan kode SQL di blog saya .
Terima kasih Bill, jawaban Anda sangat membantu dalam memulai!
sumber
Kalau diberi pilihan, saya akan menggunakan benda. Saya akan membuat objek untuk setiap catatan di mana setiap objek memiliki
children
koleksi dan menyimpannya semua dalam array assoc (/ hashtable) di mana ID adalah kuncinya. Dan kilat melalui koleksi sekali, menambahkan anak-anak ke bidang anak yang relevan. Sederhana.Tetapi karena Anda tidak bersenang-senang dengan membatasi penggunaan beberapa OOP yang baik, saya mungkin akan beralih berdasarkan:
Sunting: ini mirip dengan beberapa entri lain, tapi saya pikir ini sedikit lebih bersih. Satu hal yang akan saya tambahkan: ini sangat intensif SQL. Ini jahat . Jika Anda punya pilihan, buka rute OOP.
sumber
Ini ditulis dengan cepat, dan tidak cantik juga tidak efisien (ditambah lagi autobox banyak, mengubah antara
int
danInteger
menjengkelkan!), Tetapi bekerja.Mungkin melanggar aturan karena saya membuat objek sendiri tapi hei saya melakukan ini sebagai pengalihan dari kerja nyata :)
Ini juga mengasumsikan bahwa resultSet / tabel sepenuhnya dibaca ke dalam semacam struktur sebelum Anda mulai membangun Nodes, yang tidak akan menjadi solusi terbaik jika Anda memiliki ratusan ribu baris.
sumber
Ada solusi yang sangat baik yang mengeksploitasi representasi btree internal indeks sql. Ini didasarkan pada beberapa penelitian hebat yang dilakukan sekitar tahun 1998.
Berikut ini adalah contoh tabel (dalam mysql).
Satu-satunya bidang yang diperlukan untuk representasi pohon adalah:
Berikut adalah contoh populasi 24 simpul, yang dipesan oleh tw:
Setiap hasil pohon dapat dilakukan secara non-rekursif. Misalnya, untuk mendapatkan daftar leluhur simpul di tw = '22 '
Leluhur
Saudara kandung dan anak-anak adalah sepele - cukup gunakan bidang pemesanan pa oleh tw.
Keturunan
Misalnya set (cabang) dari node yang di-root pada tw = 17.
catatan tambahan
Metodologi ini sangat berguna ketika ada jumlah pembacaan yang jauh lebih besar daripada sisipan atau pembaruan.
Karena penyisipan, gerakan, atau pemutakhiran suatu simpul dalam pohon mengharuskan pohon untuk disesuaikan, maka perlu untuk mengunci tabel sebelum memulai dengan tindakan.
Biaya penyisipan / penghapusan tinggi karena nilai indeks tw dan sz (ukuran cabang) perlu diperbarui pada semua node setelah titik penyisipan, dan untuk semua leluhur masing-masing.
Pemindahan cabang melibatkan pemindahan nilai tw dari cabang di luar jangkauan, jadi penting juga untuk menonaktifkan batasan kunci asing saat memindahkan cabang. Ada, pada dasarnya empat permintaan yang diperlukan untuk memindahkan cabang:
Sesuaikan Kueri Pohon
Pembukaan / penutupan celah dalam struktur pohon adalah sub-fungsi penting yang digunakan dengan membuat / memperbarui / menghapus metode, jadi saya memasukkannya di sini.
Kita membutuhkan dua parameter - sebuah flag yang menunjukkan apakah kita melakukan perampingan atau upsizing, dan indeks tw node. Jadi, misalnya tw = 18 (yang memiliki ukuran cabang 5). Mari kita asumsikan bahwa kita melakukan perampingan (menghapus tw) - ini berarti bahwa kita menggunakan '-' alih-alih '+' dalam pembaruan contoh berikut.
Kami pertama-tama menggunakan fungsi leluhur (sedikit diubah) untuk memperbarui nilai sz.
Maka kita perlu menyesuaikan tw untuk mereka yang tw lebih tinggi dari cabang yang akan dihapus.
Maka kita perlu menyesuaikan induk untuk mereka yang tw pa lebih tinggi dari cabang yang akan dihapus.
sumber
Dengan asumsi Anda tahu bahwa elemen root adalah nol, inilah pseudocode untuk menghasilkan teks:
sumber
Anda dapat meniru struktur data lainnya dengan hashmap, jadi itu bukan batasan yang mengerikan. Memindai dari atas ke bawah, Anda membuat peta hash untuk setiap baris database, dengan entri untuk setiap kolom. Tambahkan masing-masing hashmaps ini ke hashmap "master", dengan kunci id. Jika ada simpul yang memiliki "induk" yang belum Anda lihat, buat entri placeholder untuknya di master hashmap, dan isi ketika Anda melihat simpul yang sebenarnya.
Untuk mencetaknya, lakukan pass-first sederhana pertama melalui data, melacak tingkat indent sepanjang jalan. Anda dapat membuatnya lebih mudah dengan menyimpan entri "anak-anak" untuk setiap baris, dan mengisinya saat Anda memindai data.
Adapun apakah ada cara "lebih baik" untuk menyimpan pohon dalam database, itu tergantung pada bagaimana Anda akan menggunakan data. Saya telah melihat sistem yang memiliki kedalaman maksimum yang diketahui yang menggunakan tabel berbeda untuk setiap level dalam hierarki. Itu masuk akal jika level di pohon tidak cukup setara setelah semua (kategori tingkat atas berbeda dari daun).
sumber
Jika peta hash bersarang atau array dapat dibuat, maka saya bisa langsung turun tabel dari awal dan menambahkan setiap item ke array bersarang. Saya harus melacak setiap baris ke simpul akar untuk mengetahui level mana dalam array bersarang untuk dimasukkan. Saya dapat menggunakan memoisasi sehingga saya tidak perlu mencari orang tua yang sama berulang kali.
Sunting: Saya akan membaca seluruh tabel menjadi array terlebih dahulu, sehingga tidak akan menanyakan DB berulang kali. Tentu saja ini tidak akan praktis jika meja Anda sangat besar.
Setelah struktur dibangun, saya harus melakukan penelusuran mendalam terlebih dahulu dan mencetak HTML.
Tidak ada cara mendasar yang lebih baik untuk menyimpan informasi ini menggunakan satu tabel (saya bisa saja salah;), dan akan senang melihat solusi yang lebih baik). Namun, jika Anda membuat skema untuk menggunakan tabel db yang dibuat secara dinamis, maka Anda membuka dunia baru dengan mengorbankan kesederhanaan, dan risiko SQL neraka;).
sumber
Jika elemen berada dalam susunan pohon, seperti yang ditunjukkan pada contoh Anda, Anda bisa menggunakan sesuatu seperti contoh Python berikut:
Apa yang dilakukan adalah mempertahankan tumpukan yang mewakili posisi saat ini di pohon. Untuk setiap elemen dalam tabel, ini akan memunculkan elemen stack (menutup div yang cocok) sampai menemukan induk dari item saat ini. Kemudian output awal simpul itu dan mendorongnya ke stack.
Jika Anda ingin menampilkan pohon menggunakan elemen indentasi daripada elemen bersarang, Anda bisa melewatkan pernyataan cetak untuk mencetak div, dan mencetak sejumlah ruang yang sama dengan beberapa kelipatan ukuran tumpukan sebelum setiap item. Misalnya, dalam Python:
Anda juga dapat dengan mudah menggunakan metode ini untuk membangun satu set daftar atau kamus bersarang.
Sunting: Saya melihat dari klarifikasi Anda bahwa nama-nama itu tidak dimaksudkan sebagai jalur simpul. Itu menunjukkan pendekatan alternatif:
Ini membangun pohon susunan tupel (!). idx [0] mewakili akar pohon. Setiap elemen dalam array adalah 2-tuple yang terdiri dari node itu sendiri dan daftar semua anak-anaknya. Setelah dibuat, Anda dapat mempertahankan idx [0] dan membuang idx, kecuali jika Anda ingin mengakses node dengan ID mereka.
sumber
Untuk Memperpanjang solusi SQL Bill Anda pada dasarnya dapat melakukan hal yang sama menggunakan array datar. Lebih jauh lagi jika semua string Anda memiliki panjang yang sama dan jumlah maksimum anak Anda diketahui (katakanlah dalam pohon biner), Anda dapat melakukannya menggunakan string tunggal (array karakter). Jika Anda memiliki jumlah anak yang sewenang-wenang, ini akan sedikit mempersulit ... Saya harus memeriksa catatan lama saya untuk melihat apa yang dapat dilakukan.
Kemudian, dengan mengorbankan sedikit memori, terutama jika pohon Anda jarang dan / atau tidak dikunci, Anda dapat, dengan sedikit indeks matematika, mengakses semua string secara acak dengan menyimpan pohon Anda, lebar pertama dalam array seperti itu (untuk biner pohon):
Anda tahu panjang tali Anda, Anda tahu itu
Saya sedang bekerja sekarang jadi tidak bisa menghabiskan banyak waktu untuk itu, tetapi dengan minat saya dapat mengambil sedikit kode untuk melakukan ini.
Kami menggunakannya untuk mencari di pohon biner yang terbuat dari kodon DNA, sebuah proses membangun pohon, kemudian kami meratakannya untuk mencari pola teks dan ketika ditemukan, meskipun indeks matematika (berbalik dari atas) kami mendapatkan simpul kembali ... sangat cepat dan efisien, tangguh pohon kami jarang memiliki node kosong, tetapi kami dapat melihat gigabytes data dalam sekejap.
sumber
Pikirkan tentang penggunaan alat nosql seperti neo4j untuk struktur hierarki. mis. aplikasi jaringan seperti linkedin menggunakan couchbase (solusi nosql lain)
Tetapi gunakan nosql hanya untuk kueri level data-mart dan bukan untuk menyimpan / memelihara transaksi
sumber