# PostgreSQL 的 update timestamp 探討 ## 前言 在處理資料時,timestamp 是很重要的資料.想必大家也都會在建立table時會使用,並且會有類似created_at, updated_at.有的會用一個欄位,只放最新的變動時刻,這樣建立時也包含在內.也有使用兩個欄位分別對應建立與最新的變動時刻. ## 來看一下MySQL的方式 MySQL有提供很便利語法,可以參考這段例子:[傳送門](https://dbfiddle.uk/Yhx6LdVn) 可以看到最後的例子中,建立與變動的時刻有差異了. MySQL在建立table時,欄位就可以指定 on update, 十分地便利.不愧為貼心小棉襖. ## PostgreSQL的方式 ```sql= create table t0105a ( id int generated always as identity primary key , val int , ts timestamp default current_timestamp on update current_timestamp , dt datetime default current_timestamp on update current_timestamp ); ERROR: 42601: syntax error at or near "on" LINE 4: , ts timestamp default current_timestamp on update current_t... ``` 恩...很貼心的跟我們說哪裡語法錯了. 也就是說 PostgreSQL 不支援在欄位指定on update的語法. ### 自己寫個trigger ```sql= create table t0105a ( id int generated always as identity primary key , val int , ts timestamp default current_timestamp ); create or replace function track_t0105a_ts() returns trigger language plpgsql as $code$ begin new.ts = clock_timestamp(); return new; end; $code$; create trigger tri_bu_t0105a before update on t0105a for each row execute function track_t0105a_ts(); insert into t0105a(val) values (1),(2),(3); select * from t0105a; id | val | ts ----+-----+---------------------------- 1 | 1 | 2024-01-05 16:55:33.752251 2 | 2 | 2024-01-05 16:55:33.752251 3 | 3 | 2024-01-05 16:55:33.752251 (3 rows) update t0105a set val = val + 1; select * from t0105a; id | val | ts ----+-----+---------------------------- 1 | 2 | 2024-01-05 16:56:13.136066 2 | 3 | 2024-01-05 16:56:13.136142 3 | 4 | 2024-01-05 16:56:13.136156 (3 rows) ``` ### PostgreSQL 的 moddatetime extension 因為這個trigger 寫法很簡單,但是每次都要寫對應的,所以就提供了一個 extension 叫 moddatetime.提供了一個函數,可以輸入欄位名稱. ```sql= create extension moddatetime; create table t0105b ( id int generated always as identity primary key , val int , ts1 timestamp default clock_timestamp() , ts2 timestamp default null ); create trigger tri_bu_t0105b_ts2 before update on t0105b for each row execute function moddatetime(ts2); insert into t0105b(val) values (1),(2),(3); select * from t0105b; id | val | ts1 | ts2 ----+-----+----------------------------+------ 1 | 1 | 2024-01-05 17:04:20.323484 | NULL 2 | 2 | 2024-01-05 17:04:20.329817 | NULL 3 | 3 | 2024-01-05 17:04:20.329836 | NULL (3 rows) update t0105b set val = val + 1; select * from t0105b; id | val | ts1 | ts2 ----+-----+----------------------------+-------------------------- 1 | 2 | 2024-01-05 17:04:20.323484 | 2024-01-05 17:05:01.5984 2 | 3 | 2024-01-05 17:04:20.329817 | 2024-01-05 17:05:01.5984 3 | 4 | 2024-01-05 17:04:20.329836 | 2024-01-05 17:05:01.5984 (3 rows) ``` 可以看到是使用current_timestamp的方式. 雖然減少寫trigger function.但是還是要指定trigger.提供了統一的函數. ### 設計哲學的差異 MySQL著重於平易近人,提供各種便利性. PostgreSQL著重整體的統一性. 以這個例子來說,當我們想知道系統中有哪些欄位會有自動的補齊或是修正的功能在update時. 在PostgreSQL只要去查 information_schema 下的trigger,或是查pg_catalog.pg_trigger, (當然還要適當的join其他系統表,這裡先不展開.) 就能夠全部找到. 但是在MySQL的情況下,MySQL也是有trigger的功能的, 這時候除了查詢trigger以外,還要想到這個便利的 on update . 哪種方式較好? 這沒有優劣.只有我們對其理解更深,更能夠掌握,能駕馭,就都是好的. ## 雲端不能建立extension的情況 有些雲端服務,提供的PostgreSQL,會是固定不能自行建立extension. 這時候可以自行開發類似 moddatetime function 的 function. 回顧之前我寫的直轉橫的分享中,有使用了json_populate_record(). 對應table的type, 而在trigger中會有 new 這個虛擬的record. 看看怎樣的組合運用. ```sql= create or replace function modts() returns trigger language plpgsql as $code$ declare colname name; begin if (tg_nargs = 1) then colname = tg_argv[0]; else raise exception 'modts(colname) (只)需要一個參數'; end if; return json_populate_record(new, json_build_object(colname, clock_timestamp())); end; $code$; create table t0105c ( id int generated always as identity primary key , val int , ts1 timestamp default clock_timestamp() , ts2 timestamp default null ); create trigger tri_bu_t0105c_ts2 before update on t0105c for each row execute function modts(ts2); insert into t0105c(val) values (1),(2),(3); select * from t0105c; id | val | ts1 | ts2 ----+-----+----------------------------+------ 1 | 1 | 2024-01-05 17:52:07.640035 | NULL 2 | 2 | 2024-01-05 17:52:07.640646 | NULL 3 | 3 | 2024-01-05 17:52:07.640658 | NULL (3 rows) update t0105c set val = val + 1; select * from t0105c; id | val | ts1 | ts2 ----+-----+----------------------------+---------------------------- 1 | 2 | 2024-01-05 17:52:07.640035 | 2024-01-05 17:52:42.955641 2 | 3 | 2024-01-05 17:52:07.640646 | 2024-01-05 17:52:42.957856 3 | 4 | 2024-01-05 17:52:07.640658 | 2024-01-05 17:52:42.957914 (3 rows) ``` 這樣我們就完成了一個通用的modts(colname)trigger function. ### 這樣是否較好? 通用的trigger function,這樣我們在查詢trigger時,可以看到使用的都是相同的.就不需要再去一一檢視.以軟體工程的一種角度來看,達到通用性,統一性.而且使用函數組合,感覺就很優雅. 但是! 優雅是要付出對應的代價的.雖然使用的資料量不大,但是畢竟也是要組合與展開.在大量資料需要更新的情況下,效能是否會比簡單粗暴的直接 ```sql= new.ts = clock_timestamp(); ``` 為低呢? 我們其實不必非黑即白,當某些table確實需要注意效能時,就用自己的trigger function吧.當然我們取名時可以有好的規範,提高理解與管理便利性.而其他table 可以使用通用型的. ## 結語 PostgreSQL與MySQL都是很優秀的資料庫,裡面有許多功能,可以讓我們使用起來更靈活方便.