Mencoba postgres ltree

Sudah cukup umum untuk menggunakan self-refrerencing foreign key untuk membuat hirarki (tree) di dalam relational database. Kita memerlukan suatu cara untuk mengakses hirarki tersebut dengan mudah. Cara-cara yang penulis ketahui antara lain:

  • Jika menggunakan postgres, bisa menggunakan query rekursif.
  • Tabel closure. Kita cukup menggunakan query “biasa”, bukan rekursif. Perlu dicatat bahwa kita perlu menjaga closure table tetap up-to-date dengan setiap perubahan di table utama.

Beberapa versi terakhir, postgres menyertakan tipe data ltree. Kita bisa menambahkan kolom bertipe ini langsung pada table hirarki. Tipe data ini querynya mirip dengan regex, yaitu pencocokan pola string. Mari kita coba.

Pertama kita buat dulu database baru yang memiliki ekstensi ltree.

CREATE DATABASE treant;
CREATE EXTENSION ltree; -- on the database above

Lalu buat tabel beserta data-datanya. Demi keringkasan, kita hanya akan membuat tipe data ltree saja di tabel tersebut tanpa parent_id dan sebagainya.

CREATE TABLE test (path ltree);
INSERT INTO test VALUES ('Politics');
INSERT INTO test VALUES ('Politics.Local');
INSERT INTO test VALUES ('Politics.Local.Elections');
INSERT INTO test VALUES ('Politics.International');
INSERT INTO test VALUES ('Politics.International.Africa');
INSERT INTO test VALUES ('Politics.International.Europe');
INSERT INTO test VALUES ('Politics.International.US');

INSERT INTO test VALUES ('Sports');
INSERT INTO test VALUES ('Sports.Events');
INSERT INTO test VALUES ('Sports.Events.FIFAWorldCup');
INSERT INTO test VALUES ('Sports.Events.TenkaichiBudokai');
INSERT INTO test VALUES ('Sports.Racing');
INSERT INTO test VALUES ('Sports.Racing.Formula1');
INSERT INTO test VALUES ('Sports.Racing.MotoGP');
INSERT INTO test VALUES ('Sports.Racing.Nascar');
INSERT INTO test VALUES ('Sports.Football');
INSERT INTO test VALUES ('Sports.Football.Asia');
INSERT INTO test VALUES ('Sports.Football.Europe');
INSERT INTO test VALUES ('Sports.Football.Oceania');
INSERT INTO test VALUES ('Sports.MartialArts');
INSERT INTO test VALUES ('Sports.MartialArts.Boxing');
INSERT INTO test VALUES ('Sports.MartialArts.MMA');

CREATE INDEX path_gist_idx ON test USING gist(path);
CREATE UNIQUE INDEX path_idx ON test USING btree(path);

Sekarang kita bisa mulai bereksperimen. Misalnya kita ingin Sports.Events beserta semua keturunannya.

treant=> SELECT path FROM test WHERE path <@ 'Sports.Events';
              path              
--------------------------------
 Sports.Events
 Sports.Events.FIFAWorldCup
 Sports.Events.TenkaichiBudokai
(3 rows)

Atau Politics.Local.Elections beserta semua leluhurnya.

treant=> SELECT path FROM test WHERE path @> 'Politics.Local.Elections';
           path           
--------------------------
 Politics
 Politics.Local
 Politics.Local.Elections
(3 rows)

Beberapa operasi ltree juga dapat menerima array. Misalnya kita ingin Politics.International dan Sports.Football beserta semua keturunan mereka:

treant=> SELECT path FROM test WHERE path <@ array['Politics.International', 'Sports.Football']::ltree[];
             path              
-------------------------------
 Politics.International
 Politics.International.Africa
 Politics.International.Europe
 Politics.International.US
 Sports.Football
 Sports.Football.Asia
 Sports.Football.Europe
 Sports.Football.Oceania
(8 rows)

Sebagai contoh terakhir, berikut adalah semua path yang mengandung label Europe:

treant=> SELECT path FROM test WHERE path ~ '*.Europe.*';
             path              
-------------------------------
 Politics.International.Europe
 Sports.Football.Europe
(2 rows)

Masih banyak operasi-operasi lainnya yang berhubungan dengan struktur hirarki. Lebih lengkapnya bisa dilihat di dokumentasinya.

Jadi? Apakah kita harus menggunakan ltree? Semua memiliki trade-off, there is no silver bullet. Mari kita bandingkan ulang semua pilihan:

  1. Query rekursif. Ini lebih sulit dari pilihan lainnya, tetapi tidak ada kompleksitas tambahan karena segalanya terkandung di dalam self-referencing foreign-key (parent_id, etc).
  2. Tabel closure. Masih menggunakan SQL query, tetapi lebih mudah dari opsi pertama. Butuh maintain tabel closure nya agar selalu terupdate.
  3. ltree. Opsi yang paling mudah untuk melakukan query pada hirarki. Juga perlu menjaga agar path tiap row selalu terupdate.

Jika pembaca ada yang sudah berpengalaman dengan SQL, tentu opsi pertama bisa menjadi pilihan.

Dalam kebanyakan aplikasi, query hirarki jarang berdiri sendiri. Misalnya kita menyimpan struktur website ke dalam pohon hirarki di database. Biasanya query yang dilakukan menyentuh entitas lain, seperti dokumen dalam sebagian cabang hirarki, maupun akses permission pada struktur atau dokumen-dokumen di dalamnya. Query akan menjadi semakin kompleks dan butuh waktu lebih untuk menyusunnya. Di tambah dengan maintenance cost beserta kemungkinan duplikasi (karena sulit dimengerti), hal ini menyebabkan opsi kedua dan ketiga menjadi cukup menggiurkan.

Jadi pertanyaannya worth it kah untuk menambah kompleksitas untuk kemudahan di kemudian hari? Seandainya sistem sudah solid dan hanya akan perlu sedikit tambahan query hirarki mungkin jawabannya tidak. Jika masih banyak yang perlu dikembangkan apalagi dengan jumlah developer yang banyak, tentu usaha yang disemai akan membuahkan hasil yang cukup besar karena banyaknya developer lain yang terbantu. Tugas kita adalah menemukan treshold tersebut.

Bagaimana menurut kalian?

Mencoba postgres ltree

Tinggalkan Balasan

Isikan data di bawah atau klik salah satu ikon untuk log in:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s