最可靠方式是用row_number()窗口函数按user_id分组、created_at降序编号取rn=1,可完整保留最新订单所有字段;误用子查询max()易因null、重复时间或版本兼容性导致结果遗漏或多行。

用窗口函数替代子查询更可靠
直接在 WHERE 里套 SELECT MAX(created_at) 子查询,通常会出错——因为 MySQL 5.7 默认不支持在子查询中引用外部表字段(报错 ERROR 1054: Unknown column ... in field list),即使能运行,也容易因 GROUP BY 策略或 NULL 订单导致结果遗漏。
真正稳妥的做法是用窗口函数 ROW_NUMBER() 标记每用户的订单序号:
SELECT user_id, order_id, created_at
FROM (
SELECT user_id, order_id, 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确保按用户分组排序 -
ORDER BY created_at DESC保证最新订单排第一 - 如果同用户有多笔同一秒创建的订单,
ROW_NUMBER()会任意选一个;需确定性结果时,加order_id DESC作为第二排序条件
MySQL 5.7 或更低版本怎么办
老版本不支持窗口函数,只能用关联子查询,但必须确保子查询返回单值且可索引:
SELECT o1.user_id, o1.order_id, o1.created_at FROM orders o1 WHERE o1.created_at = ( SELECT MAX(o2.created_at) FROM orders o2 WHERE o2.user_id = o1.user_id );
- 必须给
(user_id, created_at)建联合索引,否则性能急剧下降 - 如果某用户所有订单的
created_at都为NULL,该用户不会出现在结果中(MAX(NULL)返回NULL,而= NULL永远不成立) - 存在时间相同多笔订单时,会全部返回——这不是“最近一笔”,而是“所有最近时间的订单”
用 LEFT JOIN 模拟“排除更晚订单”逻辑
这个写法兼容所有 SQL 版本,语义清晰,且天然规避 NULL 时间问题:
SELECT o1.user_id, o1.order_id, o1.created_at FROM orders o1 LEFT JOIN orders o2 ON o1.user_id = o2.user_id AND o1.created_at
- 核心思想:找不出“同一用户、时间更晚”的订单,那它就是最新的
- 对
created_at为NULL的记录也有效(NULL 为 FALSE,不会匹配到 o2) - 需要索引
(user_id, created_at),否则 JOIN 会全表扫描 - 如果存在重复时间,仍可能返回多行;如需严格一行,再加
AND o1.order_id > o2.order_id并配合MIN(o1.order_id)
别忽略时间精度和时区问题
生产环境常见坑不是语法,而是数据本身:
-
created_at是DATETIME还是TIMESTAMP?后者受时区影响,跨服务器查可能错位 - 应用写入时用了
NOW(),但数据库时区设为 UTC,而业务按本地时间理解“最近”,结果对不上 - 高并发下多个订单毫秒级时间相同,
ROW_NUMBER()或MAX()都无法保证一致性,得靠业务层生成唯一单调递增 ID(比如雪花 ID)参与排序
真正难的不是写出能跑的 SQL,而是确认你定义的“最近”和业务方理解的“最近”是否指向同一行数据。










