Postgresql 的 ltree 處理階層式資料的好幫手
階層式的資料,在日常中隨處可見.但是要怎樣儲存處理,
有幾種常見的方式, Adjacency List, 可以透過遞迴的
方式,在 Postgresql Oracle 以及現在8版的 MySQL
都可以處理.
另一種是 Nested Set 的方式,無需特殊功能,
新增或移動節點時,自給自足.
另一個方式是 Materialized Path,把路徑存起來,用空間換時間.
Postgresql 的 ltree 用此方式,方便大家使用此種方式,來儲存處理階層式資料.
另外還提供了 ltree_plpython3u , 可以整合進 plpython.
首先要建立 extension
create extension ltree;
CREATE EXTENSION
 
安裝好以後,可以在 contrib/ltree/ 目錄下找到 ltreetest.sql 
裡面會有一個簡單的範例.也是官網文件上的例子.
我們這次改用先看一些應用,再來探討運算子與函數.
create table tree1013a (
  id smallint generated always as identity
, node text not null
, path ltree not null
);
insert into tree1013a (node, path) values
('A', 'A'),
('B', 'A.B'),
('C', 'A.C'),
('D', 'A.B.D'),
('E', 'A.C.E'),
('F', 'A.C.F');
     A
  +--+--+
  B     C
  +   +-+-+
  D   E   F
大致上如上圖所示.
select * from tree1013a;
+----+------+-------+
| id | node | path  |
+----+------+-------+
|  1 | A    | A     |
|  2 | B    | A.B   |
|  3 | C    | A.C   |
|  4 | D    | A.B.D |
|  5 | E    | A.C.E |
|  6 | F    | A.C.F |
+----+------+-------+
(6 rows)
若是用 Adjacency List 要求出這棵小樹, Node A 底下(包含A)共有哪些節點? 
需要用遞迴語法,一來語法繁瑣,二來遞迴思考需要一段適應期.
在 ltree 有一個運算子, ltree @> ltree , return boolean
左邊的ltree 是否為 右邊ltree的祖先.
我們用它來協助計算吧.
select count(*)
  from tree1013a
 where 'A'::ltree @> path;
+-------+
| count |
+-------+
|     6 |
+-------+
列出 C節點 子樹下各節點
with ctree as (
select path cpath
  from tree1013a
 where node = 'C'
)
select t.*
  from tree1013a t
     , ctree
 where cpath @> path;
 
+----+------+-------+
| id | node | path  |
+----+------+-------+
|  3 | C    | A.C   |
|  5 | E    | A.C.E |
|  6 | F    | A.C.F |
+----+------+-------+
(3 rows)
也可以用 反向的 後裔運算子 <@
with ctree as (
select path cpath
  from tree1013a
 where node = 'C'
)
select t.*
  from tree1013a t
     , ctree
 where path <@ cpath;
+----+------+-------+
| id | node | path  |
+----+------+-------+
|  3 | C    | A.C   |
|  5 | E    | A.C.E |
|  6 | F    | A.C.F |
+----+------+-------+
(3 rows)
還可以用字串函數 strpos() 這招,注意到我們使用 ::varchar 轉型.
select *
  from tree1013a
 where strpos(path::varchar, 'C') = 3;
+----+------+-------+
| id | node | path  |
+----+------+-------+
|  3 | C    | A.C   |
|  5 | E    | A.C.E |
|  6 | F    | A.C.F |
+----+------+-------+
(3 rows)
接著來把 子樹 刪除
delete
  from tree1013a
 where 'A.C'::ltree @> path;
DELETE 3
剩下
select *
  from tree1013a;
+----+------+-------+
| id | node | path  |
+----+------+-------+
|  1 | A    | A     |
|  2 | B    | A.B   |
|  4 | D    | A.B.D |
+----+------+-------+
(3 rows)
rollback;
ROLLBACK
select *
  from tree1013a;
+----+------+-------+
| id | node | path  |
+----+------+-------+
|  1 | A    | A     |
|  2 | B    | A.B   |
|  3 | C    | A.C   |
|  4 | D    | A.B.D |
|  5 | E    | A.C.E |
|  6 | F    | A.C.F |
+----+------+-------+
(6 rows)
因為我在 psqlrc 中設定, \set AUTOCOMMIT OFF
所以沒有 commit 之前,這道 delete 是可以 rollback 的.
若沒這樣設定的,請注意先加上  begin transaction , 或是
直接下  \set AUTOCOMMIT OFF
確認變更後下 commit;
將 C 節點以下分家獨立.
先將 C 獨立為 root
這個先別急著下喔
update tree1013a
   set path = 'C'
 where path = 'A.C';
然後將原本 C 以下節點一個接一個下 update ....
當然我們不會這樣做,那是 SQL沒學好,迴圈跑到老.
聰明的SQL寫法,先要理解有哪些手段,而不是迴圈硬幹.
既然是層級資料,有一個回報層級的函數也是很合理的.
nlevel(ltree) , return integer
深度優先排序
select node
     , path
     , nlevel(path)
  from tree1013a
 order by path;
+------+-------+--------+
| node | path  | nlevel |
+------+-------+--------+
| A    | A     |      1 |
| B    | A.B   |      2 |
| D    | A.B.D |      3 |
| C    | A.C   |      2 |
| E    | A.C.E |      3 |
| F    | A.C.F |      3 |
+------+-------+--------+
(6 rows)
廣度優先排序
select node
     , path
     , nlevel(path)
  from tree1013a
 order by 3;
+------+-------+--------+
| node | path  | nlevel |
+------+-------+--------+
| A    | A     |      1 |
| B    | A.B   |      2 |
| C    | A.C   |      2 |
| D    | A.B.D |      3 |
| E    | A.C.E |      3 |
| F    | A.C.F |      3 |
+------+-------+--------+
(6 rows)
這時候要是有根據層級切割path的函數, 就好像 substr(),
最好是叫做 subpath 而且能指定 offset
像這樣 subpath(ltree, offset) 
就好辦多了.
select node
     , path
     , nlevel(path)
     , subpath(path, 1)
  from tree1013a
 where nlevel(path) >= 2
 order by 3;
+------+-------+--------+---------+
| node | path  | nlevel | subpath |
+------+-------+--------+---------+
| B    | A.B   |      2 | B       |
| C    | A.C   |      2 | C       |
| D    | A.B.D |      3 | B.D     |
| E    | A.C.E |      3 | C.E     |
| F    | A.C.F |      3 | C.F     |
+------+-------+--------+---------+
(5 rows)
既然有了 nlevel(), subpath() 兩個函數,
我們來看 node C 也就是 path 'A.C'::ltree 底下的
select node
     , subpath(path, 1)
     , subpath(path, nlevel('A.C'::ltree) -1)
  from tree1013a
 where path <@ 'A.C'::ltree;
+------+---------+---------+
| node | subpath | subpath |
+------+---------+---------+
| C    | C       | C       |
| E    | C.E     | C.E     |
| F    | C.F     | C.F     |
+------+---------+---------+
(3 rows)
我們先加入一個節點 G ,在節點 E 底下,用來提高一點複雜度
insert into tree1013a (node, path) values
('G', 'A.C.E.G');
select node
     , subpath(path, nlevel('A.C'::ltree) -1)
  from tree1013a
 where path <@ 'A.C'::ltree;
+------+---------+
| node | subpath |
+------+---------+
| C    | C       |
| E    | C.E     |
| F    | C.F     |
| G    | C.E.G   |
+------+---------+
(4 rows)
update tree1013a
   set path = subpath(path, nlevel('A.C'::ltree)-1) 
 where path <@ 'A.C'::ltree;
 
UPDATE 4
select * from tree1013a;
+----+------+-------+
| id | node | path  |
+----+------+-------+
|  1 | A    | A     |
|  2 | B    | A.B   |
|  4 | D    | A.B.D |
|  3 | C    | C     |
|  5 | E    | C.E   |
|  6 | F    | C.F   |
|  7 | G    | C.E.G |
+----+------+-------+
(7 rows)
commit; 
確認變化!
今天我們先探討到這裡,明天繼續愛與勇氣的冒險.
在Postgresql 我們還可以很任性的使用 emoji tree, 來當欄位名稱.