前些天对悠游卡储值时,加值机有提供一个最近六次的纪录查询功能.
类似这种功能,在电商查询最近消费纪录等等,都带给消费者便利.
进而想以这个功能来做以下的探讨.
create table mrt_cstmr (
cardid text not null
);
insert into mrt_cstmr values
('A123'),('A456'),('A789');
create table mrt_tap_dtl (
cardid text not null
, ts timestamp not null
, stname text not null
, primary key (cardid, ts)
);
insert into mrt_tap_dtl
select c.cardid
, '2021-01-01 06:00:00'::timestamp
+ interval '1 day' * n
+ interval '1 minute' * ceil(random() * 1080)::int
, (array['新店','七张', '景美', '万隆'
, '公馆', '古亭', '西门', '北门'
, '中山', '松山', '象山', '大安'
]::text[])[ceil(random() * 12)]
from mrt_cstmr c
, generate_series(1, 8) as g(n);
-- 取最後六笔
select *
from (select *
, rank() over(partition by cardid order by ts desc) as rnk
from mrt_tap_dtl) a
where rnk <= 6;
-- 查询计画可用以下语法
explain (analyze, verbose, costs, buffers, timing, summary)
select *
from (select *
, rank() over(partition by cardid order by ts desc) as rnk
from mrt_tap_dtl) a
where rnk <= 6;
--上面是很标准的步骤,单一使用查询时语法如下
explain (analyze, verbose, costs, buffers, timing, summary)
select *
from (select *
, rank() over(partition by cardid order by ts desc) as rnk
from mrt_tap_dtl
where cardid = 'A123') a
where rnk <= 6;
需要进行排序(sort).这需要消耗资源,增加执行的时间,随着资料量增大, 消耗的资源会相对增加.
为了控制资源消耗,保持适当的反应速度,常见的方式可将历史资料移到另外的table 或是 database,线上的table 维持一季度或是数个月的资料.
但是消费者上次消费或操作可能是去年或更早,若能够将最近N笔的纪录,适当保留,类似 cache 的概念,可以提供更好的服务.甚至可以减少对纪录表(如上面例子中 mrt_tap_dtl)的查询, 纪录表只做insert , 进而减少 lock 等资源消耗.
为了要做此类似 cache 的功能,我们需要建立另一个 table, 将每个卡号的最後消费或操作纪录保存.先做最後一笔,後面再来进化.
这时候有两种常用的方式可以选择,一种是 insert 到记录表时,也对 cache 表做相关操作.
因为 cache 表只保留最新的,所以当cardid 是第一次进入 cache表时,要做 insert,後续操作为 update.
这样的方式是直觉,但是每个对纪录表做 insert 的程序,都需要确保进行两段操作,若有程序遗漏对 cache 表操作,就容易产生不一致.
另一种方式是使用trigger.由资料库的机制来确保,避免遗漏.
至於insert 与 update 结合的语法,在不同资料库,语法略有差异.
MySQL 部分可以参考
https://dev.mysql.com/doc/refman/5.7/en/insert-on-duplicate.html
PostgreSQL
https://docs.postgresql.tw/reference/sql-commands/insert
里面的 ON CONFLICT Clause
https://docs.postgresql.tw/reference/sql-commands/insert#on-conflict-clause
--先建立 cache 用途的 table
create table mrt_tap_cach (
cardid text not null primary key
, last_stname text not null
, last_ts timestamp not null
);
--接着来写 trigger function
create or replace function mrt_tap_dtl_insert()
returns trigger
language plpgsql
as $$
begin
insert into mrt_tap_cach(cardid, last_stname, last_ts)
values (new.cardid, new.stname, new.ts)
on conflict (cardid) do update
set last_stname = new.stname
, last_ts = new.ts
;
return new;
end;
$$;
--接着建立 trigger
create trigger mrt_tap_dtl_insert_trig
after insert on mrt_tap_dtl
for each row
execute procedure mrt_tap_dtl_insert();
接着来做测试
insert into mrt_tap_dtl
select c.cardid
, '2021-02-01 06:00:00'::timestamp
+ interval '1 day' * n
+ interval '1 minute' * ceil(random() * 1080)::int
, (array['新店','七张', '景美', '万隆'
, '公馆', '古亭', '西门', '北门'
, '中山', '松山', '象山', '大安'
]::text[])[ceil(random() * 12)]
from mrt_cstmr c
, generate_series(1, 3) as g(n);
--这样会输入 9 笔.(3个卡号,每个 3 笔).
--先来看 mrt_tap_dtl,里面新的资料
select *
from (select *
, rank() over(partition by cardid order by ts desc) as rnk
from mrt_tap_dtl) a
where rnk <= 3;
cardid | ts | stname | rnk
--------+---------------------+--------+-----
A123 | 2021-02-04 12:59:00 | 古亭 | 1
A123 | 2021-02-03 14:54:00 | 公馆 | 2
A123 | 2021-02-02 08:34:00 | 七张 | 3
A456 | 2021-02-04 20:37:00 | 七张 | 1
A456 | 2021-02-03 14:50:00 | 中山 | 2
A456 | 2021-02-02 23:21:00 | 象山 | 3
A789 | 2021-02-04 10:15:00 | 公馆 | 1
A789 | 2021-02-03 15:06:00 | 古亭 | 2
A789 | 2021-02-02 20:21:00 | 北门 | 3
(9 rows)
--再来看 mrt_tap_cach 保留的最後操作资料.
select *
from mrt_tap_cach;
cardid | last_stname | last_ts
--------+-------------+---------------------
A123 | 古亭 | 2021-02-04 12:59:00
A456 | 七张 | 2021-02-04 20:37:00
A789 | 公馆 | 2021-02-04 10:15:00
(3 rows)
有效的纪录了!
至此并无太多新的技术,保留最後一笔的功能 trigger 也是属於广泛应用.
接着来思考一下,怎样保留最後 N 笔,在此先以 6 笔为例.
目前在 mrt_tap_cach 中,除了卡号,我们建立了两个栏位,保留站名与时间戳.
若要保留6笔,是否再建立 10 个栏位,也就是 5 组, 但是怎样做 推陈出新 ??
这类操作对 SQL 语法来说并不自然.
是否有什麽好的方式,提供一个资料结构,来做推陈出新.
首先我们来看 PostgreSQL 是有 array 的资料型态.
可以设法使用 array 来存放,再做推陈出新的操作.
另外我们也知道 PostgreSQL可以使用 PL/Python.
可以用 PL/Python 来写 function 与 trigger.
透过 array 可以将资料传给 PL/Python 的函数, 再利用 Python 处理 List
强大的功能.看来似乎有点眉目了.
另外查找 Python 的函数库, 在 collections 中有一个 deque
https://docs.python.org/zh-tw/3/library/collections.html#collections.deque
搭配使用,就可以很方便的做限制长度,而且有推陈出新的功能,无需重新造轮子.相关搭配技术似乎找齐了.
首先我们将 mrt_tap_cach 增加一个 text[] ,也就是text型态的 array .
alter table mrt_tap_cach
add column last_n text[];
--接着来写 trigger function. 因为 mrt_tap_dtl_insert_trig 呼叫的
--mrt_tap_dtl_insert() 使用了 insert on conflict, 实际上会有
--insert 与 update 两种操作.
create or replace function mrt_tap_cach_insert_update()
returns trigger
language plpython3u
as $$
from collections import deque
d = deque(maxlen = 6)
if TD['new']['last_n'] != None:
for elem in TD['new']['last_n']:
d.append(elem)
d.appendleft([TD['new']['last_stname'], TD['new']['last_ts']])
TD['new']['last_n'] = list(d)
rv = 'MODIFY'
return rv
$$;
--建立两个 trigger
create trigger mrt_tap_cach_insert_trig
before insert on mrt_tap_cach
for each row
execute procedure mrt_tap_cach_insert_update();
create trigger mrt_tap_cach_update_trig
before update on mrt_tap_cach
for each row
execute procedure mrt_tap_cach_insert_update();
测试!
--先将 mrt_tap_dtl 与 mrt_tap_cach 的资料 truncate
truncate table mrt_tap_dtl;
truncate table mrt_tap_cach;
insert into mrt_tap_dtl
select c.cardid
, '2021-03-01 06:00:00'::timestamp
+ interval '1 day' * n
+ interval '1 minute' * ceil(random() * 1080)::int
, (array['新店','七张', '景美', '万隆'
, '公馆', '古亭', '西门', '北门'
, '中山', '松山', '象山', '大安'
]::text[])[ceil(random() * 12)]
from mrt_cstmr c
, generate_series(1, 3) as g(n);
--来看一下 mrt_tap_cach 的内容
select *
from mrt_tap_cach;
-[ RECORD 1 ]-----------------------------------------------------------------------------------------
cardid | A123
last_stname | 北门
last_ts | 2021-03-04 20:34:00
last_n | {{北门,"2021-03-04 20:34:00"},{中山,"2021-03-03 20:47:00"},{七张,"2021-03-02 13:53:00"}}
-[ RECORD 2 ]-----------------------------------------------------------------------------------------
cardid | A456
last_stname | 新店
last_ts | 2021-03-04 19:20:00
last_n | {{新店,"2021-03-04 19:20:00"},{万隆,"2021-03-03 13:23:00"},{公馆,"2021-03-02 11:16:00"}}
-[ RECORD 3 ]-----------------------------------------------------------------------------------------
cardid | A789
last_stname | 北门
last_ts | 2021-03-04 17:41:00
last_n | {{北门,"2021-03-04 17:41:00"},{新店,"2021-03-03 06:30:00"},{万隆,"2021-03-02 23:31:00"}}
如同预期,再来每个 cardid 增加 4 笔,观察长度限制.
insert into mrt_tap_dtl
select c.cardid
, '2021-03-04 06:00:00'::timestamp
+ interval '1 day' * n
+ interval '1 minute' * ceil(random() * 1080)::int
, (array['新店','七张', '景美', '万隆'
, '公馆', '古亭', '西门', '北门'
, '中山', '松山', '象山', '大安'
]::text[])[ceil(random() * 12)]
from mrt_cstmr c
, generate_series(1, 4) as g(n);
select *
from mrt_tap_cach;
...
-[ RECORD 3 ]--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
cardid | A789
last_stname | 北门
last_ts | 2021-03-08 23:47:00
last_n | {{北门,"2021-03-08 23:47:00"},{象山,"2021-03-07 15:31:00"},{新店,"2021-03-06 20:52:00"},{大安,"2021-03-05 15:17:00"},{北门,"2021-03-04 17:41:00"},{新店,"2021-03-03 06:30:00"}}
--保留到 {新店,"2021-03-03 06:30:00"} 而 {万隆,"2021-03-02 23:31:00"} 已经被推出去了!
至此已经达到保留最後 6 笔的操作纪录的功能.若想要增加保留的笔数,只要修改 mrt_tap_cach_insert_update()
trigger function 即可,也不需要重新建立trigger,也不用修改 table, 影响极小.
create or replace function mrt_tap_cach_insert_update()
returns trigger
language plpython3u
as $$
from collections import deque
d = deque(maxlen = 8)
if TD['new']['last_n'] != None:
for elem in TD['new']['last_n']:
d.append(elem)
d.appendleft([TD['new']['last_stname'], TD['new']['last_ts']])
TD['new']['last_n'] = list(d)
rv = 'MODIFY'
return rv
$$;
insert into mrt_tap_dtl
select c.cardid
, '2021-03-15 06:00:00'::timestamp
+ interval '1 day' * n
+ interval '1 minute' * ceil(random() * 1080)::int
, (array['新店','七张', '景美', '万隆'
, '公馆', '古亭', '西门', '北门'
, '中山', '松山', '象山', '大安'
]::text[])[ceil(random() * 12)]
from mrt_cstmr c
, generate_series(1, 9) as g(n);
select *
from mrt_tap_cach;
...
-[ RECORD 3 ]-----------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
cardid | A789
last_stname | 象山
last_ts | 2021-03-24 13:18:00
last_n | {{象山,"2021-03-24 13:18:00"},{象山,"2021-03-23 16:40:00"},{西门,"2021-03-22 13:41:00"},{北门,"2021-03-21 21:08:00"},{古亭,"2021-03-20 07:44:00"},{公馆,"2021-03-19 13:11:00"},{景美,"2021-03-18 17:26:00"},{大安,"2021-03-17 09:10:00"}}
以上是综合运用了 trigger , plpython, array 等功能,来达到一个简单轻量的 last n in-table cache 功能.
trigger 或是 plpython 结合运用,可以扩展使用弹性.进而减少对table的查询,提高整体的效能.
MITRE ATT&CK 纪录资安攻击与情资分享资料库,可先参考从红队角度看 MITRE ...
Agenda 资安宣言 测试环境与工具 技术原理与程序码 References 最後 资安宣言 撰写...
今天,产生了一个想法、或说好奇:是否有什麽原因,让原本在学习表现上有开放可能性的人,变成不主动积极了...
上一篇提到改变其他脚本的全域变数 这一篇针对场景间保留数据的方法作探讨。 在unity换场景(sce...
Colab连结 昨天探讨了 Batch size 的问题和前天的 Warm-up 问题後,其实在我心...