# 巧用 array 處理 tags 續篇
## tag處理的一些情況
### 查詢的情況
常見到使用tag查詢是點單一tag,少數是可以選數個tag,然後再點查詢.
雖然查詢多tag對使用者來說,使用體驗較好,但是使用此方式的網站較少.
一來查詢方式組SQL可能較為麻煩,二來可能也是效能考量.
### tag的管理
在上篇的例子中,我們有建立一個tags的table,並且有做unique index.
這樣可以確保tag的唯一性,通常用在較為嚴謹的系統,例如物料屬性,
希望不要過於發散,使用勾選的方式等等.
另一種情境是可以讓使用者自行建立,例如一些討論型態的網站,如ithelp,或是blog等等.
當不需要很嚴謹的系統,我們可以進一步反正規化,直接將tag的文字型態存到標的table,當然也可以在輸入或選擇介面上做控制,這時候查詢就不需要再跟tag table 做 join, 取得tag 的 tag_name or tag_val.
## tag 放到標的table的方式
```sql=
alter table videos_array
add column txt_tags text[];
with t1 as (
select va.vid_id, t.tag_name
from videos_array va
cross join unnest(va.vid_tags) as tag_id
join tags t
using (tag_id)
), t2 as (
select vid_id, array_agg(tag_name) as txttags
from t1
group by vid_id
)
update videos_array va2
set txt_tags = t2.txttags
from t2
where va2.vid_id = t2.vid_id;
select * from videos_array;
-[ RECORD 1 ]-------------------------------------------
vid_id | 1
vid_number | MEYD-844
vid_tags | {1,2,3,4,5,6,7,8}
txt_tags | {熟女,人妻,巨乳,泳裝,單體作品,高清,獨家,4K}
-[ RECORD 2 ]-------------------------------------------
vid_id | 2
vid_number | WAAA-293
vid_tags | {1,3,7,8,9,10,11,12}
txt_tags | {熟女,巨乳,獨家,4K,女教師,痴女,大屁股,中出}
```
## array 直接查詢
```sql=
select *
from videos_array
where txt_tags @> array['熟女', '人妻'];
-[ RECORD 1 ]-------------------------------------------
vid_id | 1
vid_number | MEYD-844
vid_tags | {1,2,3,4,5,6,7,8}
txt_tags | {熟女,人妻,巨乳,泳裝,單體作品,高清,獨家,4K}
select *
from videos_array
where txt_tags @@> array['%教師%'];
-[ RECORD 1 ]-------------------------------------------
vid_id | 2
vid_number | WAAA-293
vid_tags | {1,3,7,8,9,10,11,12}
txt_tags | {熟女,巨乳,獨家,4K,女教師,痴女,大屁股,中出}
```
可以看到使用了 @>, @@> 兩個運算子, 還能做 wildcard search!
### 修正資訊:
感謝葉賢大大提供回饋!
@@> 部分是安裝 parray_gin 之後才有的.
## 人生就是會遇到那個但是
上面的例子看起來不錯,但是資料量大時....
使用資料庫,大家都知道可以建立index,提高查詢效能.
當我們有使用tag table時,可以透過它的index.
目前使用array 直接在標的table放資料,我們就需要在建立能夠處理array的index.
上一篇中,有建立了gin 型態的index.現在就來使用看看.
## GIN Index
```sql=
create table videos_array2 (
vid_id int not null generated always as identity primary key
, vid_number text not null
, vid_tags int[]
, txt_tags text[]
);
insert into videos_array2(vid_number, vid_tags, txt_tags)
select vid_number, vid_tags, txt_tags
from videos_array va
, generate_series(1, 5000);
create index on videos_array2 using gin (vid_tags);
analyze videos_array2;
-- 因為資料是大量重複的,正常情況下,優化器不會使用index
-- 會選擇Seq Scan, 所以我們需要強制使用
-- 這裡主要是功能性的測試,而不花費時間去取得大量的影片tags
SET enable_seqscan TO off;
explain
select *
from videos_array2
where vid_tags @> array[1,3,5];
QUERY PLAN
---------------------------------------------------------------------------------------------
Bitmap Heap Scan on videos_array2 (cost=70.75..404.25 rows=5000 width=185)
Recheck Cond: (vid_tags @> '{1,3,5}'::integer[])
-> Bitmap Index Scan on videos_array2_vid_tags_idx (cost=0.00..69.50 rows=5000 width=0)
Index Cond: (vid_tags @> '{1,3,5}'::integer[])
```
## GIN Index 在text型態的array 使用情況
由上面的例子,看來好像不錯喔,那我們接著試試看,text型態的array 建立index.
```sql=
create index on videos_array2 using gin (txt_tags);
explain
select *
from videos_array2
where txt_tags @> array['熟女','巨乳','單體作品'];
QUERY PLAN
------------------------------------------------------------------------------------
Seq Scan on videos_array2 (cost=10000000000.00..10000000396.00 rows=10 width=185)
Filter: (txt_tags @> '{熟女,巨乳,單體作品}'::text[])
JIT:
Functions: 2
Options: Inlining true, Optimization true, Expressions true, Deforming true
```
## 人生何處不但是
看來是無法很好的處理,成本還很大....
GIN Index 要處裡text 型態,好像不能直接使用啊.....
我們來看看一篇有名關於GIN的文章
[Understanding Postgres GIN Indexes: The Good and the Bad](https://pganalyze.com/blog/gin-index)
裡面有提到可以使用tsvector的方式,還有GIN的後繼者RUM.
但是要使用tsvector還要再加工,而且對我們中文使用者來說,不是很好用.
## Open Source 的力量讓世界更美好
為了更方便的處理GIN與text array,有人貢獻了
[parray_gin](https://github.com/theirix/parray_gin) 這個extension
來看看怎樣使用,安裝過程我省略了.make, sudo make install搞定.
```sql=
drop index videos_array2_txt_tags_idx;
create extension parray_gin;
create index on videos_array2 using gin (txt_tags parray_gin_ops);
explain
select *
from videos_array2
where txt_tags @> array['熟女','巨乳','單體作品'];
QUERY PLAN
-------------------------------------------------------------------------------------------
Bitmap Heap Scan on videos_array2 (cost=48.08..82.44 rows=10 width=185)
Recheck Cond: (txt_tags @> '{熟女,巨乳,單體作品}'::text[])
-> Bitmap Index Scan on videos_array2_txt_tags_idx (cost=0.00..48.08 rows=10 width=0)
Index Cond: (txt_tags @> '{熟女,巨乳,單體作品}'::text[])
explain
select *
from videos_array2
where txt_tags @@> array['%女%','巨%'];
QUERY PLAN
-------------------------------------------------------------------------------------------
Bitmap Heap Scan on videos_array2 (cost=48.08..82.44 rows=10 width=185)
Recheck Cond: (txt_tags @@> '{%女%,巨%}'::text[])
-> Bitmap Index Scan on videos_array2_txt_tags_idx (cost=0.00..48.08 rows=10 width=0)
Index Cond: (txt_tags @@> '{%女%,巨%}'::text[])
```
可以看到這樣就很方便的可以直接使用了.
## 結語
PostgreSQL array 搭配 GIN Index, 加上parray_gin extension,
可以讓我們在處理tag時,很方便直觀的使用.
## 感謝
感謝許多在PostgreSQL及Open Source貢獻的人,雖然不見得有很好聽的頭銜,但是默默的付出,讓世界更美好.