这是一个艰难的问题,我一直在与之抗争。我有一张名为
action_events
的表,用于存储 event_name
和 timestamp
:
CREATE TABLE action_events (
id SERIAL PRIMARY KEY,
timestamp TIMESTAMP NOT NULL,
event_name VARCHAR(255) NOT NULL
);
我想获取具有给定名称的事件的最新“连胜”。
“连续”是事件至少发生一次的连续天数。一个事件可能一天发生不止一次。大陷阱:连胜还应考虑到给定的时区。
使用这个小提琴: https://www.db-fiddle.com/f/4jyoMCicNSZpjMt4jFYoz5/7828
鉴于“MST”时区的“运动”查询,我预计连胜是:
连胜数 | 姓名 | 时区 | 开始日期 | 结束日期 |
---|---|---|---|---|
13 | “运动” | “MST” | 2023-02-18 09:00:00 | 2023-02-30 09:00:00 |
即使连胜在一个月前结束,它仍应显示为最近的连胜。
Plain
timestamp
数据不知道时区。当我们不知道时间戳数据的时区时,给定的时区是没有意义的。timestamptz
以避免歧义和额外的转换。
纯 SQL。查询不会比这更快捷:
SELECT sum(ct) AS streak_count
, 'exercise' AS event_name
, 'MST' AS timezone
, min(min_ts) AS start_date
, max(max_ts) AS end_date
FROM (
SELECT *, the_day - row_number() OVER (ORDER BY the_day)::int AS streak
FROM (
SELECT (timestamp AT TIME ZONE 'UTC' AT TIME ZONE 'MST')::date AS the_day
, count(*) AS ct
, min(timestamp) AS min_ts
, max(timestamp) AS max_ts
FROM action_events
WHERE event_name = 'exercise'
GROUP BY 1
) sub1
) sub2
GROUP BY streak
ORDER BY end_date DESC
LIMIT 1;
sub1
立即仅过滤感兴趣的
event_name
-示例中的event_name = 'exercise'
。timestamp AT TIME ZONE 'UTC'
产生 timestamptz
.... AT TIME ZONE 'MST'
然后在 'MST' 处返回相应的 timestamp
。参见:
投射到
::date
(the_day
),然后按此分组。sub2
要识别条纹,只需从
integer
(键入the_day
!)中减去行号(键入date
!)。参见:
连续几天产生相同(否则无意义)的一天。
(
sub1
和 sub2
可以合并,但那会很笨重。)
SELECT
对每条条纹进行聚合,取计数总和、最小值中的最小值和最大值中的最大值。 按最小值(或最大值,结果相同)降序排列并取第一行(
LIMIT 1
)。在结果中,
start_date
和end_date
代表原始时间戳(UTC)。您可能希望在“MST”处显示时间戳。你没说。
没有定义最小条纹长度,所以它可能只是一行。
但是因为这会处理给定事件的所有行,所以它不能很好地扩展到很多行。
从最新的行开始并循环直到遇到间隙会(快得多)快。虽然没有最小连胜长度要求,但我们不会出错。
演示 PL/pgSQL 函数:
CREATE OR REPLACE FUNCTION f_latest_streak(
INOUT name text
, INOUT timezone text
, OUT steak_count int
, OUT start_date timestamp
, OUT end_date timestamp)
LANGUAGE plpgsql STRICT PARALLEL SAFE STABLE AS
$func$
DECLARE
_day_start timestamp;
_start_date timestamp;
_streak_step int;
BEGIN
-- get end & UTC time for start of latest day at given time zone
SELECT max(a.timestamp)
, date_trunc('day', max(a.timestamp) AT TIME ZONE 'UTC' AT TIME ZONE timezone)
AT TIME ZONE 'UTC' AT TIME ZONE 'UTC' -- sic!
INTO end_date, _day_start
FROM action_events a
WHERE event_name = name;
IF NOT FOUND THEN -- no rows at all
RETURN;
END IF;
-- get count for first day
SELECT count(*)::int, min(timestamp)
INTO steak_count, start_date
FROM action_events a
WHERE a.event_name = name -- careful with naming conflicts!
AND a.timestamp >= _day_start;
-- more days?
LOOP
SELECT count(*)::int, min(timestamp)
INTO _streak_step, _start_date
FROM action_events a
WHERE a.event_name = name -- careful with naming conflicts!
AND a.timestamp >= _day_start - interval '1 day'
AND a.timestamp < _day_start;
IF _streak_step = 0 THEN -- streak ends here
RETURN;
ELSE
steak_count := steak_count + _streak_step;
start_date := _start_date;
_day_start := _day_start - interval '1 day';
END IF;
END LOOP;
END
$func$;
电话:
SELECT * FROM f_latest_streak('exercise', 'MST');
(event_name, timestamp)
上的多列索引将使函数fast.
你应该熟悉 PL/pgSQL 才能玩这个。