group by 不保证组内顺序,即使配 order by 也只影响最终结果排序,无法确保取到每组最新记录;正确做法是用窗口函数 row_number()(mysql 8.0+)或 join 子查询(5.7 兼容),并为 (user_id, created_at) 建联合索引。

为什么 GROUP BY 直接配 ORDER BY 不能拿到每组最新记录
MySQL 的 GROUP BY 不保证组内行的顺序,即使你写 ORDER BY created_at DESC,它也只影响最终结果集排序,不会让 SELECT * 拿到组内排序后的第一行。常见错误是这么写:
SELECT *, MAX(created_at) FROM orders GROUP BY user_id;
这会报错(在严格模式下)或返回不可预测的其他列值——MAX(created_at) 是对的,但 order_id、status 等字段可能来自任意一条记录。
用窗口函数 ROW_NUMBER() 最直观可靠(MySQL 8.0+)
给每组按时间倒序编号,再过滤出序号为 1 的行。这是语义最清晰、可读性最强的做法。
- 必须用
PARTITION BY user_id定义分组维度 -
ORDER BY created_at DESC确保最新记录排第一;若时间相同,建议加二级排序如id DESC避免不确定性 - 不能在同一个查询中直接
WHERE rn = 1,需套一层子查询或 CTE
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC, id DESC) AS rn
FROM orders
) t WHERE rn = 1;
兼容 MySQL 5.7 的方案:用 JOIN + 子查询找最大时间
先算出每组最大时间,再关联原表取完整记录。注意要处理时间重复的情况——如果同一用户有两条记录时间完全相同,这个方法可能返回多条。
- 子查询必须用
(user_id, created_at)联合匹配,不能只靠created_at,否则会跨用户误匹配 - 如果存在时间相同但需要唯一结果,得额外加条件(比如取
id最大的那条),这时不如升级到 8.0 用窗口函数 - 性能上,给
(user_id, created_at)建联合索引能显著加速
SELECT o1.* FROM orders o1 INNER JOIN ( SELECT user_id, MAX(created_at) AS max_time FROM orders GROUP BY user_id ) o2 ON o1.user_id = o2.user_id AND o1.created_at = o2.max_time;
别踩 GROUP_CONCAT + SUBSTRING_INDEX 这个坑
有人用 GROUP_CONCAT(id ORDER BY created_at DESC) 拼接再截取首项,看似能绕过限制,但隐患很多:
-
GROUP_CONCAT默认长度上限是 1024 字符,长 ID 或多字段拼接极易被截断 - 字符串截取无法还原原始数据类型(比如把
INT变成字符串再转回,还可能丢精度) - 无法安全处理含逗号、换行等特殊字符的字段内容
- 可读性和维护性差,出问题很难调试
除非你明确控制数据范围且版本低于 5.7,否则不推荐。
真正麻烦的地方往往不是语法怎么写,而是没想清楚「最新」的定义是否包含时间重复场景,以及线上表的数据量和索引覆盖情况——这两点直接决定窗口函数是否快,或者 JOIN 方案会不会触发全表扫描。











