最直接可靠的方式是用ROW_NUMBER()配合PARTITION BY user_id ORDER BY created_at DESC, order_id DESC取rn=1;必须建(user_id, created_at)联合索引,避免GROUP BY或子查询性能陷阱。

用窗口函数 ROW_NUMBER() 最直接可靠
MySQL 8.0+ 支持窗口函数,这是查“每个用户最新订单”的首选方案。核心思路是按 user_id 分组,再按 created_at(或 order_id)降序排序,取每组第一行。
常见错误是只用 GROUP BY user_id 配合 MAX(created_at),但这样拿不到完整订单字段(比如 order_id、amount),MySQL 5.7 及以前会报错或返回不可靠数据。
正确写法示例:
SELECT user_id, order_id, amount, created_at
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) t
WHERE rn = 1;- 必须用
PARTITION BY user_id确保分组边界清晰 - 如果存在同一秒多个订单,建议补上二级排序,如
ORDER BY created_at DESC, order_id DESC避免非确定性结果 -
ROW_NUMBER()比RANK()更稳妥:即使时间相同,也能强制唯一编号,避免漏掉记录
MySQL 5.7 或更低版本只能用关联子查询
老版本不支持窗口函数,得靠相关子查询或 JOIN + 聚合。性能较差,但兼容性强。
典型写法是先查出每个用户的最大时间,再关联原表取完整记录:
SELECT o1.user_id, o1.order_id, o1.amount, o1.created_at FROM orders o1 INNER JOIN ( SELECT user_id, MAX(created_at) AS max_created FROM orders GROUP BY user_id ) o2 ON o1.user_id = o2.user_id AND o1.created_at = o2.max_created;
- 如果多个订单时间完全相同,这个写法会返回多条——这可能是业务需要,也可能不是,得看场景
- 若必须只取一条(比如按
order_id最大),需在子查询里用(user_id, created_at, order_id)三元组做联合最大值,或改用LEFT JOIN自连接方式 - 注意
created_at字段要有索引,否则GROUP BY+MAX()会全表扫描
别踩 GROUP BY 隐式依赖的坑
MySQL 5.7 默认开启 sql_mode=ONLY_FULL_GROUP_BY,此时 SELECT * 配 GROUP BY user_id 会直接报错:Expression #2 of SELECT list is not in GROUP BY clause。
有人会关掉这个模式来“绕过”,但后果严重:
- MySQL 会随机返回某一行的非聚合字段值,结果不可预测
- 升级 MySQL 或迁移数据库时极易暴雷
- 哪怕当前看着“对”,也只是碰巧——不是逻辑正确
所以,永远不要依赖 SELECT *, MAX(created_at) FROM orders GROUP BY user_id 这类写法。
用 ORDER BY ... LIMIT 1 在单用户场景下才安全
如果只是查“某个指定用户”的最新订单,LIMIT 1 是最轻量、最高效的方式:
SELECT * FROM orders WHERE user_id = 123 ORDER BY created_at DESC, order_id DESC LIMIT 1;
- 务必加
ORDER BY,否则LIMIT 1返回顺序无保证 - 复合排序(时间 + ID)能确保结果确定性,尤其当时间精度不足时
- 这个写法不能扩展成“所有用户”,强行套
GROUP BY或子查询会失效
真正难的是“批量取最新”——窗口函数解决它,老版本就得接受性能折损和逻辑绕行。时间字段是否带索引、是否存在重复值、MySQL 版本是否可控,这几个点卡住了大多数人的实际落地。


















