订单表要取每个用户最近一单,写了 group by user_id 加 max(created_at),时间对了,金额和状态却对不上,因为 max 只保留时间,其他列是随便取的。
这类需求用窗口函数:不减少行数,按分组算一个值,再在外层筛。
先认排名三兄弟
- ROW_NUMBER:1、2、3,绝无并列
- RANK:并列之后跳号,1、1、3
- DENSE_RANK:并列不跳号,1、1、2
取最新一条的标准写法
SELECT * FROM (SELECT t.*, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC, id DESC) rn FROM orders t) x WHERE rn = 1。第二个排序键别省,时间戳相同时只按时间排,结果不稳定,今天跑和明天跑可能不是同一条。
累计和环比
SUM(amount) OVER (PARTITION BY user_id ORDER BY created_at) 是累计,默认窗口从第一行到当前行,时间戳相同的行会一起算进去。要严格逐行,写 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。环比用当前 amount 减 LAG(amount) OVER (ORDER BY created_at),LAG 的第二个参数能指定往前几行。
为什么不能写在 where 里
SQL 的执行顺序是 FROM、WHERE、GROUP BY、HAVING、窗口函数、ORDER BY。窗口在 WHERE 之后才算,所以 where rn = 1 一定报错,必须包一层子查询或者 CTE。
版本和性能
MySQL 8.0、PostgreSQL、SQL Server、Oracle 都支持,MySQL 5.7 没有,只能用自连接凑。大表按没有索引的列分区会触发排序,执行计划里看到 WindowAgg 前面挂着 Sort,就该考虑补联合索引。
能用窗口就别写一堆自连接,但窗口不是免费的,该建的索引还得建。