GROUP BY + MAX 无法获取整行最新记录,因非聚合字段未在 GROUP BY 中会导致结果不可靠;正确做法是用 ROW_NUMBER() 窗口函数按组内时间倒序编号后取 rn=1 的行。

GROUP BY + MAX 无法直接拿到整行最新记录
很多人以为 GROUP BY 配合 MAX(created_at) 就能取出每组的最新完整记录,但这是错的。SQL 标准里,SELECT 列表中未出现在 GROUP BY 中的非聚合字段是非法的(MySQL 5.7+ 严格模式下会报错:Expression #2 of SELECT list is not in GROUP BY clause)。即使旧版 MySQL 允许执行,结果也**不可靠**——数据库可能随机返回某一行的其他字段值,和 MAX() 对应的那行完全不匹配。
正确做法:用窗口函数 ROW_NUMBER() 排序取 Top 1
最通用、语义清晰的方式是使用窗口函数。对每组按时间倒序编号,再筛选出序号为 1 的行:
SELECT id, user_id, status, created_at
FROM (
SELECT id, user_id, status, created_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) t
WHERE rn = 1;-
PARTITION BY user_id对应你要分组的字段(比如用户 ID) -
ORDER BY created_at DESC确保最新时间排第一;若时间可能重复,建议加二级排序如id DESC避免不确定性 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持该写法;SQLite 3.25+ 也支持
兼容老版本 MySQL(5.7 及以下)的替代方案
如果不能升级或数据库不支持窗口函数,可用自连接或子查询,但要注意性能和 NULL 边界:
SELECT o1.* FROM orders o1 LEFT JOIN orders o2 ON o1.user_id = o2.user_id AND o1.created_at < o2.created_at WHERE o2.id IS NULL;
- 原理是:找不出“同 user_id 下有更新时间”的记录,即为最新
- 必须确保
created_at非 NULL,否则o1.created_at 比较结果为 UNKNOWN,导致漏数据 - 索引建议建在
(user_id, created_at)上,否则多表关联时容易慢 - 如果存在并列最新(相同时间),此方法可能返回多行;需额外去重或改用相关子查询
别用 GROUP BY + 非聚合字段“碰运气”
即使 MySQL 5.6 允许下面这种写法,也不要依赖它:
SELECT user_id, MAX(created_at), status -- ❌ status 值不确定 FROM orders GROUP BY user_id;
因为 status 不在 GROUP BY 里,也不参与聚合,它的值由引擎随意选取,和 MAX(created_at) 所在的那条记录大概率不一致。开发阶段可能“看起来对”,上线后数据量变大或优化器调整,结果就变了。
真正要拿最新记录,核心就一条:先明确“最新”的定义(时间?ID?),再用确定性方式定位到那一行——不是靠 GROUP BY 带着侥幸心理捞字段。

















