最可靠做法是用ROW_NUMBER()配合PARTITION BY分组和ORDER BY时间降序,再通过子查询或CTE过滤rn=1;必须建(a,b)联合索引保障性能,MySQL 5.7等旧版本不支持需换方案。

用 ROW_NUMBER() 按时间排序取每组第一条最可靠
直接用 ROW_NUMBER() 配合 ORDER BY created_at DESC 是最通用、最不容易出错的做法。它不依赖字段唯一性,也不怕时间相同的情况(会按任意但确定的顺序排)。
常见错误是写成 ORDER BY created_at ASC 然后取 rn = 1,结果拿到的是最早记录;或者漏写 PARTITION BY,导致全表只排一次序。
- 必须用
PARTITION BY group_id(替换成你的分组字段)划分组 -
ORDER BY updated_at DESC或created_at DESC,确保最新在前 - 外层查询过滤
WHERE rn = 1,不能在窗口内加WHERE
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY updated_at DESC) AS rn
FROM orders
) t WHERE rn = 1;
RANK() 和 DENSE_RANK() 在时间重复时行为不同
如果同一组里多条记录 updated_at 完全相同,ROW_NUMBER() 会强行给它们不同序号(比如 1,2,3),而 RANK() 会给并列第一(1,1,1),DENSE_RANK() 也是(1,1,1),但后续是 2 而不是跳到 4。
这意味着:如果你希望「所有最新时间的记录都保留」,就得用 RANK() 或 DENSE_RANK(),否则只拿到其中一条。
-
RANK():并列时跳序号(1,1,3),适合强调“名次”场景 -
DENSE_RANK():并列时不跳(1,1,2),更贴近自然计数 - 只要没明确要求保留并列项,一律优先用
ROW_NUMBER()
SELECT * FROM (
SELECT *,
RANK() OVER (PARTITION BY category ORDER BY updated_at DESC) AS rk
FROM products
) t WHERE rk = 1;MySQL 8.0+ 支持,但旧版本得绕开窗口函数
MySQL 5.7 及更早版本不支持窗口函数,硬套 ROW_NUMBER() 会报错 ERROR 1064。PostgreSQL、SQL Server、Oracle、BigQuery 都支持,但语法细节略有差异(比如 PostgreSQL 允许在子查询中直接用别名过滤,MySQL 不行)。
- 确认版本:
SELECT VERSION(); - MySQL 5.7 替代方案:用
JOIN+ 子查询找每组最大时间,再关联原表 - SQLite 3.25+ 才支持窗口函数,旧版也得回退到关联方案
别指望 LIMIT 1 加 GROUP BY 能解决问题——SQL 标准不允许这种组合,MySQL 5.7 开启 ONLY_FULL_GROUP_BY 时直接报错。
性能关键点:分组字段和排序字段要有联合索引
没索引时,PARTITION BY a ORDER BY b DESC 可能触发全表扫描 + 文件排序,数据量一过百万就明显变慢。
- 最佳索引是
(a, b),注意顺序:先分组字段,再排序字段 - 如果排序字段是
updated_at DESC,索引定义里不用写DESC(MySQL 8.0+ 支持,但多数版本忽略该声明) - 避免在
ORDER BY里用表达式,比如DATE(updated_at),会导致索引失效
执行前看一眼 EXPLAIN,确认 type 是 ref 或 range,而不是 ALL。

















