必须用lag()配合排名函数(如dense_rank())按用户分组、时间排序获取上期排名,再相减得变动;order by在两者中语义不同:排名函数按分数排次序,lag()按时间取前一行,否则逻辑错误。

直接用 LAG() 和排名函数组合就能算出用户排名变动,不用写子查询或自连接——这是最轻量、最可靠的方案。
怎么用 LAG() 拿到上期排名
排名变动 = 当前排名 − 上期排名,核心是先拿到“上期”值。LAG(rank_col) 就是干这个的,但它必须和排名函数在同一个窗口定义里嵌套使用:
-
LAG()的OVER子句要和排名函数完全一致:同PARTITION BY user_id(按用户分组)、同ORDER BY report_date(按时间升序),否则会拉错行 - 别在外部再套一层
SELECT去“先算排名、再查上期”,那样容易因数据稀疏(比如用户隔两周才发一次动态)导致JOIN匹配失败 - 首条记录没有“上期”,
LAG()默认返回NULL,得用COALESCE(LAG(rank_col) OVER (...), 0)或CASE WHEN ROW_NUMBER() = 1 THEN 0 ELSE ... END处理,否则变动列全是空
选 ROW_NUMBER() 还是 RANK() 决定变动语义
排名函数选错,变动值就失去业务意义:
- 用
ROW_NUMBER():两人同分也强制给不同名次(1,2),变动值反映的是“顺序位移”,适合内容流排序、Feed 刷新逻辑 - 用
RANK():同分并列(1,1,3),变动值为 0 表示“仍并列第一”,为 −2 表示“从第3升到第1”,但若从第1掉到第2,可能变成 1→3(跳号导致),容易误导 - 日常分析推荐
DENSE_RANK():并列不跳号(1,1,2),变动值更平滑,比如 1→2 就是稳稳下降一位,不会突然跳成 1→3
PARTITION BY 必须按用户 + 时间粒度切分
只写 PARTITION BY user_id 是错的——那会把用户所有历史快照混在一起排,根本不是“本期 vs 上期”。真实场景要:
- 先聚合出每个用户的周期指标(如周发文数、周互动分),生成带
user_id、week_start、score的宽表 -
PARTITION BY user_id ORDER BY week_start:确保每个用户独立看趋势,时间升序保证LAG()拉的是上周 - 如果想看“在全体用户中的相对变动”,则
PARTITION BY week_start ORDER BY score DESC算全局排名,再套LAG()拉上周的全局名次——注意这时PARTITION BY是时间,不是用户
ORDER BY 里没处理 NULL 会导致排名漂移
社交数据常有缺失:某周用户停更,score 为 NULL。不同数据库对 NULL 排序默认不同:
- PostgreSQL 默认
NULLS LAST(NULL排末尾),MySQL 8.0 默认NULLS FIRST(NULL排开头) - 结果就是:同一 SQL 在两个库跑,
LAG()拉的可能是NULL而非真实值,变动列全乱 - 显式声明:
ORDER BY score DESC NULLS LAST(PostgreSQL)或用COALESCE(score, 0)统一转成数值,避免隐式行为干扰
真正难的不是写对语法,而是确认“本期”和“上期”的定义是否和业务口径一致——比如周统计是自然周还是滚动7天?用户休眠期要不要计入?这些一旦错,LAG() 算出来的变动再准也没用。











