我有一张记录事件的桌子。这些事件将按每个用户每分钟进入系统一次 - 每分钟大约有数十个或数千个。但我不需要保存全部。
如果一个事件在前一个事件发生后 90 秒内发生,我想更新上一行。如果已经超过 90 秒,我想插入新行。
示例表:
create table events (
id serial,
user_id int references users(id) not null,
created_at timestamp not null default now(),
updated_at timestamp not null default now(),
...some other event columns here...
);
create index idx_events_user_id_updated_at on events (user_id, updated_at desc);
伪代码类似于:
updated_at
在最后 90 秒内:
updated_at
和 now()
有没有办法用单个 Postgres 语句来做到这一点?
我知道
on conflict
,但我认为它不适用于这个用例。 (user_id, updated_at)
对 could 可以定义为唯一约束,可用于触发 on conflict
,但时间戳是任意的。这些事件“每分钟”发生一次,但不是“恰好”每分钟发生一次(或者甚至“恰好”间隔一分钟,由于网络延迟、服务器延迟等原因,因此使用 90 秒来提供 30 秒的缓冲区)。将时间戳截断为分钟会降低该功能的实用性,因此我不想这样做只是为了更干净地处理更新插入。
我认为最好的选择是创建一个带有 upsert 块的存储过程,即:
首先假设记录存在并执行
UPDATE
。包含一个
WHERE
子句,用于检查记录是否在 90 秒前更新。
如果没有任何更新,则意味着之前没有记录或之前的记录更新时间超过 90 秒 - 无论哪种方式,您都需要执行INSERT
从客户端的角度来看,只会有一个调用(执行过程),并且大多数时候服务器上只会有一个
UPDATE
,但是有时需要第二个INSERT
。
有没有办法用单个 Postgres 语句来做到这一点?
可以使用 UPSERT 命令来完成。您需要对时间戳范围进行约束:
EXCLUSION
ALTER TABLE events
ADD CONSTRAINT user_90sec
EXCLUDE USING gist (user_id WITH =, tsrange(updated_at, updated_at + interval '90 sec') WITH &&);
timestamptz + interval
只是
STABLE
,而
timestamp + interval
是 IMMUTABLE
,这是隐含 GiST 索引(或任何与此相关的索引)所需的。查询:WITH input_rows(user_id, data) AS (
VALUES
(1, 'foo_new') -- your input here
, (2, 'bar_new')
, (3, 'baz_new')
-- more?
)
, ins AS (
INSERT INTO events (user_id, data)
SELECT user_id, data FROM input_rows
ON CONFLICT ON CONSTRAINT user_90sec DO NOTHING
RETURNING user_id
)
UPDATE events e
SET updated_at = LOCALTIMESTAMP
, data = i.data
FROM input_rows i
LEFT JOIN ins USING (user_id)
WHERE ins.user_id IS NULL
AND e.user_id = i.user_id
AND e.updated_at > LOCALTIMESTAMP - interval '90 sec';
小提琴
这假设输入中每个 user_id
最多有
行。 并且没有并发、竞争的写入。否则,由于
INSERT
和 UPDATE
之间不可避免的时间差距,可能会出现竞争条件。 (排除约束不允许
ON CONFLICT ... DO UPDATE
。)此外,输入中没有不明确的数据类型。参见:
如何在 PostgreSQL 中使用 RETURNING 和 ON CONFLICT?
tsrange
添加生成的列并以此为基础排除约束(及其隐含的 GiST 索引)可能会更快。不过,会使表格行膨胀。参见:
PostgreSQL 中的计算/计算/虚拟/派生列