GROUP BY 加 MAX(created_at) 只能获取时间戳,无法保证其他字段来自最新行,因非聚合非分组字段属未定义行为;MySQL 5.7+严格模式会报错,旧版结果随机;推荐MySQL 8.0+用ROW_NUMBER()窗口函数配合PARTITION BY user_id和ORDER BY created_at DESC取rn=1。

为什么 GROUP BY 加 MAX(created_at) 拿不到完整最新行?
直接用 GROUP BY user_id 配合 MAX(created_at) 只能拿到时间戳,拿不到对应那条记录的 id、status 等字段——因为 SQL 标准里,SELECT 列表中非聚合、非 GROUP BY 的字段是未定义行为。MySQL 5.7 严格模式下会报错:Expression #3 of SELECT list is not in GROUP BY clause;即使旧版本侥幸返回结果,数据也极可能错乱。
常见误写:
SELECT user_id, MAX(created_at), status FROM orders GROUP BY user_id;
这里 status 是随机值,不是最新那条的 status。
用 ROW_NUMBER() 窗口函数替代子查询更稳,但要注意 MySQL 版本
MySQL 8.0+ 支持窗口函数,这是目前最清晰、可读性最强的解法。它避免了关联子查询的重复扫描,执行计划通常走索引排序,性能比老式自连接好得多。
关键点:
-
PARTITION BY user_id定义分组边界 -
ORDER BY created_at DESC确保最新排第一(注意DESC,漏掉就全反了) - 外层必须过滤
rn = 1,不能在窗口内加LIMIT
示例:
SELECT user_id, id, status, created_at
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) t
WHERE rn = 1;
如果表有复合索引 (user_id, created_at),这个查询基本能走索引覆盖,速度很快。
MySQL 5.7 或更低版本只能靠相关子查询,但必须加索引,否则 O(n²)
没有窗口函数时,典型写法是主表每行触发一次子查询找该 user_id 下最大时间:
SELECT o1.* FROM orders o1 WHERE o1.created_at = ( SELECT MAX(o2.created_at) FROM orders o2 WHERE o2.user_id = o1.user_id );
这看似简洁,但极易慢:若没索引,子查询对每个 o1 行都全表扫一遍 orders;10 万行主表 × 10 万行子查询 = 100 亿次比较。
必须确保有以下任一索引:
-
INDEX(user_id, created_at)(最优,覆盖查询) -
INDEX(user_id)+INDEX(created_at)(效果差很多,优化器常选错)
另一个坑:created_at 有重复时,可能返回多条——这不是 bug,而是语义本身如此。如需唯一结果,得加第二排序字段,比如 ORDER BY created_at DESC, id DESC 并改用 ROW_NUMBER() 或自连接。
别碰 LEFT JOIN 自连接方案,除非你确认数据量极小
网上常见“左连自己找更大时间为空”的写法:
,无法利用 B+ 树索引做范围跳转,往往退化为嵌套循环,数据量过万就明显卡顿。EXPLAIN 里常看到 Type: ALL 和 Rows 指数级放大。











