DISTINCT无法实现断点去重,因其仅全局去重,无法识别按业务顺序(如user_id、event_time)划分的连续相同值段;需用双ROW_NUMBER()差值构造稳定grp标识,再按user_id、status、grp分组取首行。

为什么直接用 DISTINCT 无法满足“断点去重”需求
因为 DISTINCT 是全局去重,它不管数据的物理顺序或业务逻辑中的“断点”(比如同一用户连续登录行为中断后重新开始,应视为新序列)。而“断点去重”本质是:对每个分组内按时间/序号连续的相同值做合并,一旦中间出现不同值,就重置计数——这需要识别“连续段”,DISTINCT 做不到。
用 ROW_NUMBER 配合子查询构造“连续段标识”
核心思路是:对原始数据按业务顺序(如 user_id, event_time)排序,再分别计算两个 ROW_NUMBER():一个按全部字段分区排序,另一个按关键去重字段(如 status)加分区字段一起排序。两者的差值就是稳定不变的“段 ID”。
常见错误是只用一层 ROW_NUMBER(),或分区字段漏掉业务主键(如只分 status 不分 user_id),导致跨用户误合并。
- 假设表
user_events有字段:user_id,event_time,status - 先在子查询中生成段标识:
SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time) - ROW_NUMBER() OVER (PARTITION BY user_id, status ORDER BY event_time) AS grp FROM user_events
- 外层按
user_id,status,grp分组,取每段第一条(如MIN(event_time)或用ROW_NUMBER() = 1)
如何用子查询 + ROW_NUMBER 实现“取每段首行”
不能直接在 GROUP BY 中混用聚合和非聚合字段,所以得靠子查询套一层 ROW_NUMBER() 来标记每段内的序号。注意窗口函数必须在子查询里完成,否则外部无法引用 grp 字段。
性能上,如果数据量大,务必确保 (user_id, event_time) 和 (user_id, status, event_time) 上有索引;否则两个 ROW_NUMBER() 会触发两次全表排序。
- 完整写法示例:
SELECT user_id, status, event_time FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id, status, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time) - ROW_NUMBER() OVER (PARTITION BY user_id, status ORDER BY event_time) ORDER BY event_time) AS rn FROM user_events ) t WHERE rn = 1; - SQL Server 2022+ 可用
LAG()简化判断是否“断点”,但兼容性不如双ROW_NUMBER()方案 - 别名
grp必须在子查询中计算并暴露,否则外层无法用于PARTITION BY
容易被忽略的边界情况
空值、重复时间戳、时区转换后的 event_time 微秒差异,都会让 ROW_NUMBER() 排序结果不稳定,进而破坏“连续段”识别。尤其当 event_time 来自不同系统且未对齐精度时,看似连续的两条记录可能因毫秒差被拆成两段。
- 建议在子查询最外层加
CONVERT(DATETIME2(0), event_time)统一截断精度 - 若
status允许为NULL,PARTITION BY user_id, status会把所有NULL归为同一组,需改用PARTITION BY user_id, ISNULL(status, '##NULL##') - SQL Server 对
OVER()中表达式的支持有限,不能直接在PARTITION BY里写减法表达式,必须先算出grp再引用

















