订单表要取每个用户最近一单,写了 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,就该考虑补联合索引。

能用窗口就别写一堆自连接,但窗口不是免费的,该建的索引还得建。