PostgreSQL实现同用户同类型连续事件的10分钟间隔去重统计
PostgreSQL 实现自定义滚动事件窗口统计的方案
核心逻辑说明
你需要的统计规则本质是基于已确认有效事件触发的滚动10分钟窗口:
- 按
UserID + EventName做分组,组内所有事件按时间升序排列 - 每识别到一个有效事件,立即从该事件时间开始生成一个10分钟的拦截窗口
- 拦截窗口内的同组事件全部丢弃,不参与统计
- 当后续事件时间超过当前拦截窗口的截止时间,将该事件标记为新的有效事件,重新生成10分钟拦截窗口
这个逻辑和固定时间窗口(比如自然日、整点小时切分的窗口)有本质区别:窗口的起点完全由之前的有效事件决定,不是按固定时间刻度切分。
可直接复现的实现步骤
1. 准备测试数据
先创建临时表插入对应样例数据,方便验证结果:
CREATE TEMP TABLE user_events ( EventID INT, UserID INT, EventName VARCHAR(50), Timestamp TIMESTAMP ); INSERT INTO user_events VALUES (1,1,'search_event','2020-01-20 09:42:52'), (2,1,'search_event','2020-01-20 09:42:58'), (3,2,'search_event','2020-01-20 09:43:27'), (4,1,'checkout_event','2020-01-20 09:43:49'), (5,2,'checkout_event','2020-01-20 09:43:54'), (6,2,'search_event','2020-01-20 09:44:12'), (7,1,'search_event','2020-01-20 09:54:21'), (8,1,'search_event','2020-01-20 12:45:10'), (9,1,'search_event','2020-01-20 12:45:32'), (10,2,'booking_event','2020-01-20 12:46:52');
2. 核心统计SQL
采用PostgreSQL支持的递归CTE实现,逻辑和人手动判断的步骤完全一致,对SQL初学者友好:
WITH RECURSIVE event_valid AS ( -- 递归锚点:取每个用户、每类事件的最早一条作为初始有效事件 SELECT EventID, UserID, EventName, Timestamp, -- 计算当前有效事件对应的10分钟拦截窗口截止时间 Timestamp + INTERVAL '10 minute' AS window_end FROM ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY UserID, EventName ORDER BY Timestamp ASC ) AS row_num FROM user_events ) sorted_event WHERE row_num = 1 UNION ALL -- 递归遍历:顺着时间线找下一个符合窗口要求的有效事件 SELECT next_event.EventID, next_event.UserID, next_event.EventName, next_event.Timestamp, next_event.Timestamp + INTERVAL '10 minute' AS window_end FROM event_valid curr_valid JOIN user_events next_event ON curr_valid.UserID = next_event.UserID AND curr_valid.EventName = next_event.EventName -- 只找同分组下时间更晚的事件 AND next_event.Timestamp > curr_valid.Timestamp -- 仅保留超过当前拦截窗口的事件,作为新的有效事件 AND next_event.Timestamp >= curr_valid.window_end ) -- 输出最终有效事件 SELECT EventID, UserID, EventName, Timestamp FROM event_valid ORDER BY EventID;
运行结果验证
上面的SQL执行后,输出的有效事件完全匹配预期:
| EventID | UserID | EventName | Timestamp | 说明 |
|---|---|---|---|---|
| 1 | 1 | search_event | 2020-01-20 09:42:52 | 有效 |
| 3 | 2 | search_event | 2020-01-20 09:43:27 | 有效 |
| 4 | 1 | checkout_event | 2020-01-20 09:43:49 | 有效 |
| 5 | 2 | checkout_event | 2020-01-20 09:43:54 | 有效 |
| 7 | 1 | search_event | 2020-01-20 09:54:21 | 有效(超过Event1的10分钟窗口) |
| 8 | 1 | search_event | 2020-01-20 12:45:10 | 有效(超过Event7的10分钟窗口) |
| 10 | 2 | booking_event | 2020-01-20 12:46:52 | 有效 |
被自动过滤的无效事件完全符合预期:
- EventID 2:落在Event1的10分钟拦截窗口内
- EventID 6:落在Event3的10分钟拦截窗口内
- EventID 9:落在Event8的10分钟拦截窗口内
新手常见避坑提示
不要用LAG()窗口函数直接取上一条事件的时间做差值判断,这种写法存在逻辑漏洞:
LAG()取的是物理顺序上的紧邻上一条事件,哪怕那条事件是被前面的窗口过滤掉的无效事件- 比如连续3个同组事件:09:40(有效)、09:45(无效,在09:40的窗口内)、09:52(和09:40间隔12分钟,本该有效),用LAG判断时会拿09:52和紧邻的09:45做差,仅间隔7分钟,就会把本该有效的事件误判为无效。
- 递归CTE的判断逻辑永远和上一个有效事件的时间做对比,不存在这类逻辑错误。
内容的提问来源于stack exchange,提问作者Ashmeet Lamba
相关产品推荐
相关产品推荐

