用ROW_NUMBER()与日期差构造连续组的核心是:对用户超万交易按日期排序后,用trade_date减去序号,同组结果恒定;再按此标识分组统计天数,筛选≥3天的用户。

用 ROW_NUMBER() 和日期差构造连续组
核心思路是:把每个用户每天的交易记录按日期排序,再用日期减去序号。同一连续段内的日期减序号结果恒定——这是识别“连续”的经典 trick。
假设表叫 transactions,字段有 user_id、trade_date(DATE 类型)、amount。先筛出单日超万的记录:
SELECT user_id, trade_date FROM transactions WHERE amount > 10000
再对每个 user_id 按 trade_date 排序,生成序号:
SELECT user_id, trade_date,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY trade_date) AS rn
FROM transactions
WHERE amount > 10000关键一步:用 trade_date - INTERVAL rn DAY(MySQL)或 trade_date - rn * INTERVAL '1 day'(PostgreSQL)得到“连续组标识”。SQL Server 用 DATEADD(day, -rn, trade_date)。
按组统计天数并筛选 ≥3 天的用户
上一步的结果里,“连续组标识”相同的行就属于同一连续段。接下来只需按 user_id 和该标识分组,数天数:
- MySQL 示例(日期相减支持直接运算):
SELECT user_id FROM ( SELECT user_id, trade_date - INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY trade_date) DAY AS grp FROM transactions WHERE amount > 10000 ) t GROUP BY user_id, grp HAVING COUNT(*) >= 3 - PostgreSQL 要显式转成 interval:
trade_date - (ROW_NUMBER() OVER (...)::int * INTERVAL '1 day') - 注意:如果
trade_date是TIMESTAMP,先::DATE截断,否则同一天不同时间会被拆开
避免常见错误:重复交易、跨年、非工作日干扰
这个方法只认物理日期是否连续,不区分是否为交易日或节假日。如果业务要求“连续交易日”而非“连续自然日”,就得先补全用户所有交易日(哪怕当天没数据),再判断——那已超出窗口函数单步能解的范围。
- 同一用户同一天多笔超万交易?没问题,
COUNT(*)统计的是天数,不是笔数;但需确保GROUP BY前已去重日期,否则会虚高。加DISTINCT或用MIN(trade_date)等聚合兜底 - 跨年时
DATE - INTERVAL仍正确,无需特殊处理 - Oracle 用户注意:用
TRUNC(trade_date)替代隐式转换,且ROW_NUMBER()后必须用- NUMTODSINTERVAL(rn, 'DAY')
想返回具体是哪3天?加个 LISTAGG 或 STRING_AGG
单纯查用户 ID 很快,但业务常要验证或展示。在最终 GROUP BY 外层套一层,用聚合函数拼日期:
- PostgreSQL:
STRING_AGG(trade_date::text, ',' ORDER BY trade_date) - MySQL 8.0+:
GROUP_CONCAT(trade_date ORDER BY trade_date SEPARATOR ',') - SQL Server:
STRING_AGG(CAST(trade_date AS VARCHAR), ',') WITHIN GROUP (ORDER BY trade_date)
别忘了在子查询里把原始 trade_date 也带出来——窗口函数本身不改变行粒度,但后续 GROUP BY 会压缩,得提前保留。
真正难的不是写对语法,而是确认“连续3天”在你业务里到底指自然日还是有效交易日;后者需要先生成日期维度再左联,那已经不是窗口函数能单独扛住的事了。

















