mysql 8.0+ 应用 row_number() 窗口函数按 user_id 分组、created_at 降序编号后取 rn=1,最可靠;5.7-需用关联子查询或自连接,但易因重复时间或多值返回出错,且性能较差。

用窗口函数 ROW_NUMBER() 最直接可靠
MySQL 8.0+ 支持窗口函数,这是目前最清晰、最不容易出错的方式。核心思路是按分组排序后给每行编号,再筛出序号为 1 的记录。
常见错误是只用 GROUP BY 配合 MAX(created_at),但这无法保证其他字段(比如 id 或 status)来自同一行——MySQL 在非 ONLY_FULL_GROUP_BY 模式下会随机返回,结果不可靠。
示例:查每个 user_id 下最新一条订单
SELECT id, user_id, amount, created_at
FROM (
SELECT id, user_id, amount, 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确保最新时间排第一;若时间可能重复,建议追加id DESC避免不确定性 - 性能上,
created_at字段需有索引(复合索引(user_id, created_at)效果更佳)
MySQL 5.7 及以下只能靠关联子查询或自连接
低版本不支持窗口函数,必须用逻辑等价但更易出错的手法。最常用的是「关联最大时间」方式,但要注意 NULL 和重复时间的陷阱。
典型写法:
SELECT o1.id, o1.user_id, o1.amount, 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有多条记录时间相同,会返回多行——这不是“最新一条”,而是“所有最新时间的记录” - 若想强制只取一条(如 ID 最大的那条),得改成两层条件:
o1.created_at = ... AND o1.id = (SELECT MAX(o2.id) FROM orders o2 WHERE ... AND o2.created_at = o1.created_at) - 这个写法在大数据量时性能较差,建议对
(user_id, created_at)建联合索引
别用 GROUP BY + SELECT *,MySQL 不允许也不安全
有人尝试写 SELECT * FROM orders GROUP BY user_id ORDER BY created_at DESC,这在 MySQL 5.7 默认配置下会报错 Expression #1 of SELECT list is not in GROUP BY clause;即使关掉 ONLY_FULL_GROUP_BY,MySQL 也只保证 GROUP BY 字段确定,其余字段值是未定义的,实际运行结果完全不可预测。
这种写法看似简洁,实则埋了严重数据一致性隐患,线上环境务必禁用。
- MySQL 文档明确说明:非聚合列的值是“任意选取的”,不保证来自同一行
- 升级 MySQL 版本后可能突然报错或返回不同结果,排查成本远高于初期改用正确写法
用 LEFT JOIN 自连接也能做,但可读性差、易漏条件
原理是:对每条记录,找同分组中时间更大的记录;如果找不到,说明它就是最新的。但必须小心处理 NULL 和等值判断。
SELECT o1.* FROM orders o1 LEFT JOIN orders o2 ON o1.user_id = o2.user_id AND o1.created_at
- 关键点是
AND o1.created_at (不是 <code>),否则相等时间会误判 - 如果存在时间相同的情况,此方法仍可能返回多行;要唯一确定,需把条件加强为
o1.created_at - 执行计划里容易出现全表扫描,没有合适索引时性能急剧下降
索引设计和时间字段精度是实际落地中最容易被跳过的环节。哪怕语法写对了,缺了 (user_id, created_at) 这类覆盖索引,查询可能从毫秒变秒级;而用 DATETIME 存时间却没考虑微秒级并发写入,就可能让“最新一条”变成“其中一条”。











