最可靠方案是用row_number()窗口函数配合子查询:先按partition by分组、order by时间降序编号,再在外层筛选rn=1的记录,确保取到每组最新且完整的一行。

用窗口函数 ROW_NUMBER() 配合子查询最可靠
直接在 GROUP BY 里拿“最新一条”是行不通的——GROUP BY 本身不保留行序,也没有“最新”概念。必须借助排序能力,而 ROW_NUMBER() 是目前跨数据库兼容性较好、语义最清晰的选择。
典型写法是:先用窗口函数按分组和时间倒序编号,再在外层筛选 rn = 1 的记录。
SELECT id, user_id, created_at, status
FROM (
SELECT id, user_id, created_at, status,
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决定哪条算“最新”,确保该字段非 NULL 或已处理空值(比如用COALESCE(created_at, '1970-01-01')) - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 也支持,但旧版不行
- 如果存在并列最新(created_at 完全相同),
ROW_NUMBER()会任意选一条;要稳定结果可追加唯一字段如id:ORDER BY created_at DESC, id DESC
MySQL 5.7 及更早版本不能用窗口函数?试试相关子查询
老 MySQL 没 ROW_NUMBER(),但可以用子查询找每组最大时间,再关联原表。性能较差,数据量大时明显卡顿,但语法通用。
SELECT o1.* FROM orders o1 WHERE o1.created_at = ( SELECT MAX(o2.created_at) FROM orders o2 WHERE o2.user_id = o1.user_id );
- 这个写法假设
created_at是唯一的;如果有重复时间,可能返回多条——它拿的是“所有等于最大时间的记录”,不是严格意义的“一条” - 若必须只取一条,得再加一层去重,比如用
MIN(id)或LIMIT 1,但 MySQL 5.7 不允许子查询里用LIMIT,只能改写为连接 - 索引很关键:
(user_id, created_at)复合索引能极大提升子查询效率,否则全表扫描
GROUP BY + MAX() 只能取字段值,不能取整行
很多人误写成这样:
SELECT user_id, MAX(created_at), status FROM orders GROUP BY user_id;
这是错的。status 不在 GROUP BY 里,也不包在聚合函数中,MySQL 5.7 开启 ONLY_FULL_GROUP_BY 会直接报错:Expression #3 of SELECT list is not in GROUP BY clause;即使关掉,status 值也是随机选取的,完全不可控。
- 想靠
MAX(id)或MAX(created_at)反推整行?不行——它们只是标量值,无法定位原始记录 - 某些 MySQL 版本允许这种写法但结果无保证,上线后容易出数据错乱,别心存侥幸
- 真要这么做,必须配合
JOIN回原表,等价于上面的相关子查询思路
PostgreSQL 有更简洁的 DISTINCT ON,但仅限它自己
如果你确定只用 PostgreSQL,DISTINCT ON 是语法糖,写起来短,逻辑也直观:
SELECT DISTINCT ON (user_id) * FROM orders ORDER BY user_id, created_at DESC;
-
DISTINCT ON必须配合ORDER BY,且ORDER BY开头字段要和DISTINCT ON一致,否则报错 - 它按
user_id分组,每组取ORDER BY排序后的第一行,天然解决“最新一条”问题 - 其他数据库不支持,换库就得重写;另外,如果
user_id为空(NULL),会被当作同一组,要注意过滤
真正麻烦的不是语法怎么写,而是搞清“最新”的定义是否隐含业务约束:时间字段是否允许为空?并发写入下时间精度够不够?要不要考虑事务提交顺序?这些细节一旦没对齐,再漂亮的 SQL 也会返工。










