Mengelola Data Hirarki di PostgreSQL

Pojok Programmer Mengelola Data Hirarki di PostgreSQL

[**Database**](http://pojokprogrammer.net/tags/database) [**Tips**](http://pojokprogrammer.net/tags/tips-dan-trik) | Semua programmer yang berkonsentrasi di pengembangan aplikasi perkantoran (*office application*) pasti pernah berhadapan dengan data hirarki (***hierarchical data***) ke dalam database relasional (*relational database*). Jika sebelumnya kita mempelajari cara mengelola data [**hirarki di MySQL**](http://pojokprogrammer.net/content/mengelola-data-hirarki-di-mysql), maka pada artikel inimkita akan mempelajarinya di [PostgreSQL](http://pojokprogrammer.net/tags/postgresql). RDBMS sendiri tidak diperuntukkan untuk menyimpan data hirarki karena relasi antar table bukanlah relasi hirarki. Namun di PostgreSQL kita bisa memanfaatkan fitur ***Common Table Expression (CTE)*** untuk melakukan reksi di SQL Query. Data hirarki yang umumnya terdapat dalam aplikasi perkantoran, seperti data departemen, cabang, kategori barang, chart of accounts, dan sebagainya. Dalam artikel ini kita akan mencoba menggunakan data ***Chart of Accounts*** atau ***Perkiraan*** yang sangat diperlukan dalam sebuah aplikasi akuntansi (*Accounting System*)

(more…)

Mengelola Data Hirarki di MySQL

Pojok Programmer Mengelola Data Hirarki di MySQL

[**Database**](http://pojokprogrammer.net/tags/database) [**Tips**](http://pojokprogrammer.net/tags/tips-dan-trik) | Semua programmer yang berkonsentrasi di pengembangan aplikasi perkantoran (*office application*) pasti pernah berhadapan dengan data hirarki (***hierarchical data***) ke dalam database relasional (*relational database*) seperti MySQL. RDBMS sendiri tidak diperuntukkan untuk menyimpan data hirarki karena relasi antar table bukanlah relasi hirarki. Namun kita bisa merepresentasikan data hirarki menggunakan relasi ***self-reference***, atau relasi *foreign key* ke table yang sama.

**Data Hirarki** adalah sekumpulan data yang masing-masingnya memiliki satu ***parent*** dengan pengecualian untuk data ***root***. Masing-saing data dalam hirarki juga bisa memilik satu atau beberapa ***child***, atau tidak memiliki child untuk data pada level terendah, yang biasa disebut ***leaf***. Data hirarki yang umumnya terdapat dalam aplikasi perkantoran, seperti data departemen, cabang, kategori barang, chart of accounts, dan sebagainya. Dalam artikel ini kita akan mencoba menggunakan data ***Chart of Accounts*** atau ***Perkiraan*** yang sangat diperlukan dalam sebuah aplikasi akuntansi (*Accounting System*), seperti tampak pada gambar di bawah ini,

![Hirarki Perkiraan (Chart of Account COA Hierarchy](http://pojokprogrammer.net/sites/default/files/pictures/database/hirarki-01.png)
![Hirarki Perkiraan (Chart of Account COA Hierarchy](http://pojokprogrammer.net/sites/default/files/pictures/database/hirarki-02.png)

### Adjacency List Model

**Cara termudah** merepresentasikan data hirarki di dalam database relasional adalah dengan menggunakan model ***Senarai Ketetanggaan*** (***Adjacency List***). dalam model senarai ketetanggaan ini, setiap record memiliki pointer ke *parent* dari record tersebut. Khusus untuk *root* atau data paling atas tidak memiliki parent maka kita set NULL untuk *parent* dari *root*. Untuk contoh kasus kita, maka yang menjadi ***root*** adalah record ***Assets***, ***Liabilities***, dan ***Equities***.

**Keuntungan** penggunaan senarai ketetanggaan adalah kesederhanaannya, karena kita bisa dengan mudah melihat *parent* dari *Petty Cash* adalah *Current Assets*, yang sekaligus merupakan *child* dari *Assets*. Berikut ini adalah DDL dan DML yang kita gunakan sebagai contoh kasus.

CREATE TABLE accounts
(
  id integer  auto_increment,
  code varchar(255) NOT NULL,
  name varchar(255) NOT NULL,
  parent_id integer,
  PRIMARY KEY (id),
  FOREIGN KEY (parent_id)
      REFERENCES accounts (id) 
      ON UPDATE CASCADE ON DELETE RESTRICT
);

INSERT INTO accounts (id, code, name, parent_id) VALUES(1000, '1000', 'Assets', NULL);
INSERT INTO accounts (id, code, name, parent_id) VALUES(1100, '1100', 'Current Assets', 1000);
INSERT INTO accounts (id, code, name, parent_id) VALUES(1110, '1110', 'Petty Cash', 1100);
INSERT INTO accounts (id, code, name, parent_id) VALUES(1120, '1120', 'Savings Account', 1100);
INSERT INTO accounts (id, code, name, parent_id) VALUES(1130, '1130', 'Deposits Account', 1100);
INSERT INTO accounts (id, code, name, parent_id) VALUES(1150, '1150', 'Accounts Receivables', 1100);
INSERT INTO accounts (id, code, name, parent_id) VALUES(1200, '1200', 'Fixed Assets', 1000);
INSERT INTO accounts (id, code, name, parent_id) VALUES(1210, '1210', 'Furnitures', 1200);
INSERT INTO accounts (id, code, name, parent_id) VALUES(1220, '1220', 'Equipments', 1200);
INSERT INTO accounts (id, code, name, parent_id) VALUES(1230, '1230', 'Vehicles', 1200);
INSERT INTO accounts (id, code, name, parent_id) VALUES(1250, '1250', 'Buildings', 1200);
INSERT INTO accounts (id, code, name, parent_id) VALUES(1900, '1900', 'Other Assets', 1000);
INSERT INTO accounts (id, code, name, parent_id) VALUES(1910, '1910', 'Patents', 1900);
INSERT INTO accounts (id, code, name, parent_id) VALUES(1990, '1990', 'Other Assets', 1900);
INSERT INTO accounts (id, code, name, parent_id) VALUES(2000, '2000', 'Liabilities', NULL);
INSERT INTO accounts (id, code, name, parent_id) VALUES(2100, '2100', 'Current Liabilities', 2000);
INSERT INTO accounts (id, code, name, parent_id) VALUES(2110, '2110', 'Accrued Expenses', 2100);
INSERT INTO accounts (id, code, name, parent_id) VALUES(2120, '2120', 'Accounts Payable', 2100);
INSERT INTO accounts (id, code, name, parent_id) VALUES(2130, '2130', 'Tax Payable', 2100);
INSERT INTO accounts (id, code, name, parent_id) VALUES(2200, '2200', 'Long-Term Liabilities', 2000);
INSERT INTO accounts (id, code, name, parent_id) VALUES(2210, '2210', 'Equipments Payable', 2200);
INSERT INTO accounts (id, code, name, parent_id) VALUES(2220, '2220', 'Vehicles Payable', 2200);
INSERT INTO accounts (id, code, name, parent_id) VALUES(2240, '2240', 'Buildings Payable', 2200);
INSERT INTO accounts (id, code, name, parent_id) VALUES(2250, '2250', 'Loans Payable', 2200);
INSERT INTO accounts (id, code, name, parent_id) VALUES(3000, '3000', 'Equity', NULL);
INSERT INTO accounts (id, code, name, parent_id) VALUES(3100, '3100', 'Capital', 3000);
INSERT INTO accounts (id, code, name, parent_id) VALUES(3110, '3110', 'Founders Capital', 3100);
INSERT INTO accounts (id, code, name, parent_id) VALUES(3120, '3120', 'Invenstors Capital', 3100);
INSERT INTO accounts (id, code, name, parent_id) VALUES(3190, '3190', 'Others Capital', 3100);
INSERT INTO accounts (id, code, name, parent_id) VALUES(3200, '3200', 'Earnings', 3000);
INSERT INTO accounts (id, code, name, parent_id) VALUES(3210, '3210', 'Retained Earnings', 3200);
INSERT INTO accounts (id, code, name, parent_id) VALUES(3220, '3220', 'Current Earnings', 3200);

### Membaca Keseluruhan Hirarki

Hal **yang pasti kita hadapi** saat mengelola data hirarki adalah menampilkan keseluruhan data, biasanya berbentuk ***tree*** dengan tambahan indentasi untuk level yang lebih rendah. Cara paling mudah untuk mencapai hal ini di MySQL adalah dengan menggunakan ***self-join***. Berikut ini **query hirarki** (***hierarchical query***) untuk mendapatkan *tree* ***Assets***

SELECT t1.id, t1.code
, t1.name AS name_level1, t2.name as name_level2
, t3.name AS name_level3, t4.name as name_level4
FROM accounts AS t1
LEFT JOIN accounts AS t2 ON t2.parent_id = t1.id
LEFT JOIN accounts AS t3 ON t3.parent_id = t2.id
LEFT JOIN accounts AS t4 ON t4.parent_id = t3.id
WHERE t1.name = 'Assets'
ORDER BY t1.code, t2.code, t3.code, t4.code

Hasilnya akan tampak seperti ini

+------+------+-------------+----------------+----------------------+-------------+
| id   | code | name_level1 | name_level2    | name_level3          | name_level4 |
+------+------+-------------+----------------+----------------------+-------------+
| 1000 | 1000 | Assets      | Current Assets | Petty Cash           | NULL        |
| 1000 | 1000 | Assets      | Current Assets | Savings Account      | NULL        |
| 1000 | 1000 | Assets      | Current Assets | Deposits Account     | NULL        |
| 1000 | 1000 | Assets      | Current Assets | Accounts Receivables | NULL        |
| 1000 | 1000 | Assets      | Fixed Assets   | Furnitures           | NULL        |
| 1000 | 1000 | Assets      | Fixed Assets   | Equipments           | NULL        |
| 1000 | 1000 | Assets      | Fixed Assets   | Vehicles             | NULL        |
| 1000 | 1000 | Assets      | Fixed Assets   | Buildings            | NULL        |
| 1000 | 1000 | Assets      | Other Assets   | Patents              | NULL        |
| 1000 | 1000 | Assets      | Other Assets   | Other Assets         | NULL        |
+------+------+-------------+----------------+----------------------+-------------+
10 rows in set (0.00 sec)

### Menambahkan *Level* dan *Path*

Jika kita ingin menambahkan kolom yang berisikan level dari masing-masing record, maka kita harus lebih kreatif dalam membangun ***Hierarchical Query*** ini. Karena MySQL belum mendukung ***Common Table Expression*** untuk melakukan ***Recursive Query***, maka yang perlu kita lakukan adalah melakukan query untuk masing-masing level dan kemudian menggabungkannya menggunakan **UNION**. Perhatikan dalam query kali ini kita menggunakan **INNER JOIN**

-- Level 1
SELECT t1.id, t1.code, t1.name, 1 AS level
, t1.name AS name_level1, null as name_level2
, null as name_level3, null as name_level4
, t1.code AS path
FROM accounts AS t1
WHERE t1.parent_id is null
-- Level 2
UNION ALL
SELECT t2.id, t2.code, t2.name, 2 AS level
, t1.name AS name_level1, t2.name as name_level2
, null as name_level3, null as name_level4
, concat(t1.code,'/',t2.code) AS path
FROM accounts AS t1
JOIN accounts AS t2 ON t2.parent_id = t1.id
WHERE t1.parent_id is null
-- Level 3
UNION ALL
SELECT t3.id, t3.code, t3.name, 3 AS level
, t1.name AS name_level1, t2.name as name_level2
, t3.name AS name_level3, null as name_level4
, concat(t1.code,'/',t2.code,'/',t3.code) AS path
FROM accounts AS t1
JOIN accounts AS t2 ON t2.parent_id = t1.id
JOIN accounts AS t3 ON t3.parent_id = t2.id
WHERE t1.parent_id is null
-- Level 3
UNION ALL
SELECT t4.id, t4.code, t4.name, 4 AS level
, t1.name AS name_level1, t2.name as name_level2
, t3.name AS name_level3, t4.name as name_level4
, concat(t1.code,'/',t2.code,'/',t3.code,'/',t4.code) AS path
FROM accounts AS t1
JOIN accounts AS t2 ON t2.parent_id = t1.id
JOIN accounts AS t3 ON t3.parent_id = t2.id
JOIN accounts AS t4 ON t4.parent_id = t3.id
WHERE t1.parent_id is null
-- Ordering Result
ORDER BY path

Hasilnya akan tampak seperti ini

+------+------+-----------------------+-------+-------------+-----------------------+----------------------+-------------+----------------+
| id   | code | name                  | level | name_level1 | name_level2           | name_level3          | name_level4 | path           |
+------+------+-----------------------+-------+-------------+-----------------------+----------------------+-------------+----------------+
| 1000 | 1000 | Assets                |     1 | Assets      | NULL                  | NULL                 | NULL        | 1000           |
| 1100 | 1100 | Current Assets        |     2 | Assets      | Current Assets        | NULL                 | NULL        | 1000/1100      |
| 1110 | 1110 | Petty Cash            |     3 | Assets      | Current Assets        | Petty Cash           | NULL        | 1000/1100/1110 |
| 1120 | 1120 | Savings Account       |     3 | Assets      | Current Assets        | Savings Account      | NULL        | 1000/1100/1120 |
| 1130 | 1130 | Deposits Account      |     3 | Assets      | Current Assets        | Deposits Account     | NULL        | 1000/1100/1130 |
| 1150 | 1150 | Accounts Receivables  |     3 | Assets      | Current Assets        | Accounts Receivables | NULL        | 1000/1100/1150 |
| 1200 | 1200 | Fixed Assets          |     2 | Assets      | Fixed Assets          | NULL                 | NULL        | 1000/1200      |
| 1210 | 1210 | Furnitures            |     3 | Assets      | Fixed Assets          | Furnitures           | NULL        | 1000/1200/1210 |
| 1220 | 1220 | Equipments            |     3 | Assets      | Fixed Assets          | Equipments           | NULL        | 1000/1200/1220 |
| 1230 | 1230 | Vehicles              |     3 | Assets      | Fixed Assets          | Vehicles             | NULL        | 1000/1200/1230 |
| 1250 | 1250 | Buildings             |     3 | Assets      | Fixed Assets          | Buildings            | NULL        | 1000/1200/1250 |
| 1900 | 1900 | Other Assets          |     2 | Assets      | Other Assets          | NULL                 | NULL        | 1000/1900      |
| 1910 | 1910 | Patents               |     3 | Assets      | Other Assets          | Patents              | NULL        | 1000/1900/1910 |
| 1990 | 1990 | Other Assets          |     3 | Assets      | Other Assets          | Other Assets         | NULL        | 1000/1900/1990 |
| 2000 | 2000 | Liabilities           |     1 | Liabilities | NULL                  | NULL                 | NULL        | 2000           |
| 2100 | 2100 | Current Liabilities   |     2 | Liabilities | Current Liabilities   | NULL                 | NULL        | 2000/2100      |
| 2110 | 2110 | Accrued Expenses      |     3 | Liabilities | Current Liabilities   | Accrued Expenses     | NULL        | 2000/2100/2110 |
| 2120 | 2120 | Accounts Payable      |     3 | Liabilities | Current Liabilities   | Accounts Payable     | NULL        | 2000/2100/2120 |
| 2130 | 2130 | Tax Payable           |     3 | Liabilities | Current Liabilities   | Tax Payable          | NULL        | 2000/2100/2130 |
| 2200 | 2200 | Long-Term Liabilities |     2 | Liabilities | Long-Term Liabilities | NULL                 | NULL        | 2000/2200      |
| 2210 | 2210 | Equipments Payable    |     3 | Liabilities | Long-Term Liabilities | Equipments Payable   | NULL        | 2000/2200/2210 |
| 2220 | 2220 | Vehicles Payable      |     3 | Liabilities | Long-Term Liabilities | Vehicles Payable     | NULL        | 2000/2200/2220 |
| 2240 | 2240 | Buildings Payable     |     3 | Liabilities | Long-Term Liabilities | Buildings Payable    | NULL        | 2000/2200/2240 |
| 2250 | 2250 | Loans Payable         |     3 | Liabilities | Long-Term Liabilities | Loans Payable        | NULL        | 2000/2200/2250 |
| 3000 | 3000 | Equity                |     1 | Equity      | NULL                  | NULL                 | NULL        | 3000           |
| 3100 | 3100 | Capital               |     2 | Equity      | Capital               | NULL                 | NULL        | 3000/3100      |
| 3110 | 3110 | Founders Capital      |     3 | Equity      | Capital               | Founders Capital     | NULL        | 3000/3100/3110 |
| 3120 | 3120 | Invenstors Capital    |     3 | Equity      | Capital               | Invenstors Capital   | NULL        | 3000/3100/3120 |
| 3190 | 3190 | Others Capital        |     3 | Equity      | Capital               | Others Capital       | NULL        | 3000/3100/3190 |
| 3200 | 3200 | Earnings              |     2 | Equity      | Earnings              | NULL                 | NULL        | 3000/3200      |
| 3210 | 3210 | Retained Earnings     |     3 | Equity      | Earnings              | Retained Earnings    | NULL        | 3000/3200/3210 |
| 3220 | 3220 | Current Earnings      |     3 | Equity      | Earnings              | Current Earnings     | NULL        | 3000/3200/3220 |
+------+------+-----------------------+-------+-------------+-----------------------+----------------------+-------------+----------------+
32 rows in set (0.01 sec)

### Mencari Semua *Leaf Node*

Kita juga bisa mendapatkan semua ***leaf node***, yaitu node pada level terbawah dan tidak memiliki **child**, dengan menggunakan **LEFT JOIN**

-- Level 1
SELECT t1.id, t1.code
, t1.name AS name_level1, t2.name as name_level2
, t3.name AS name_level3, t4.name as name_level4
FROM accounts AS t1
LEFT JOIN accounts AS t2 ON t2.parent_id = t1.id
LEFT JOIN accounts AS t3 ON t3.parent_id = t2.id
LEFT JOIN accounts AS t4 ON t4.parent_id = t3.id
WHERE t1.parent_id is null
ORDER BY t1.code, t2.code, t3.code, t4.code

Hasilnya adaah seperti ini.

+------+------+-------------+-----------------------+----------------------+-------------+
| id   | code | name_level1 | name_level2           | name_level3          | name_level4 |
+------+------+-------------+-----------------------+----------------------+-------------+
| 1000 | 1000 | Assets      | Current Assets        | Petty Cash           | NULL        |
| 1000 | 1000 | Assets      | Current Assets        | Savings Account      | NULL        |
| 1000 | 1000 | Assets      | Current Assets        | Deposits Account     | NULL        |
| 1000 | 1000 | Assets      | Current Assets        | Accounts Receivables | NULL        |
| 1000 | 1000 | Assets      | Fixed Assets          | Furnitures           | NULL        |
| 1000 | 1000 | Assets      | Fixed Assets          | Equipments           | NULL        |
| 1000 | 1000 | Assets      | Fixed Assets          | Vehicles             | NULL        |
| 1000 | 1000 | Assets      | Fixed Assets          | Buildings            | NULL        |
| 1000 | 1000 | Assets      | Other Assets          | Patents              | NULL        |
| 1000 | 1000 | Assets      | Other Assets          | Other Assets         | NULL        |
| 2000 | 2000 | Liabilities | Current Liabilities   | Accrued Expenses     | NULL        |
| 2000 | 2000 | Liabilities | Current Liabilities   | Accounts Payable     | NULL        |
| 2000 | 2000 | Liabilities | Current Liabilities   | Tax Payable          | NULL        |
| 2000 | 2000 | Liabilities | Long-Term Liabilities | Equipments Payable   | NULL        |
| 2000 | 2000 | Liabilities | Long-Term Liabilities | Vehicles Payable     | NULL        |
| 2000 | 2000 | Liabilities | Long-Term Liabilities | Buildings Payable    | NULL        |
| 2000 | 2000 | Liabilities | Long-Term Liabilities | Loans Payable        | NULL        |
| 3000 | 3000 | Equity      | Capital               | Founders Capital     | NULL        |
| 3000 | 3000 | Equity      | Capital               | Invenstors Capital   | NULL        |
| 3000 | 3000 | Equity      | Capital               | Others Capital       | NULL        |
| 3000 | 3000 | Equity      | Earnings              | Retained Earnings    | NULL        |
| 3000 | 3000 | Equity      | Earnings              | Current Earnings     | NULL        |
+------+------+-------------+-----------------------+----------------------+-------------+
22 rows in set (0.00 sec)

### Kesimpulan

Meskipun **terlihat mudah**, ada beberapa hal yang perlu kita perhatikan dalam mengelola data hirarki di MySQL ini,

– Kita harus mengetahui jumlah level sejak awal
– Setiap ada penambahan level, maka kita juga harus menambahkan ***left join*** lagi ke dalam query. Perlu diketahui bahwa semakin banyak *left join* maka kinerja (*performance*) mysql akan menurun.
– Penghapusan record (*node*) tidak boleh dilakukan secara sembarangan karena ada resiko tree akan berantakan jika ada child yang mengacu ke node yang dihapus. Namun hal ini bisa diatasi dengan ***Foreign Key Constraint***.

Cara lain untuk mengembangkan query hirarki ini adalah dengan menggunakan ***stored procedure*** untuk membangun query secara dinamis sehingga bisa mengantisipasi jumlah level yang lebeih banyak dan bahkan tidak terbatas.

Semoga berguna!

.