最可靠方式是用 ROW_NUMBER() 窗口函数按组内时间倒序编号后取 rn=1;GROUP BY 无法直接选“最新一行”,非聚合字段结果未定义;旧版本可用关联子查询或自连接,但需注意性能与唯一性。

用窗口函数 ROW_NUMBER() 按时间排序取首行最可靠
直接在 GROUP BY 里选“最新一条”是无效的——SQL 标准不支持对非聚合字段做模糊选取,MySQL 5.7 严格模式下会报错 Expression #1 of SELECT list is not in GROUP BY clause。必须借助窗口函数或关联子查询。
推荐用 ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY update_time DESC),它为每组内记录按时间倒序编号,再外层筛选 rn = 1 即可拿到完整行:
SELECT id, user_id, status, update_time
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY update_time DESC) AS rn
FROM orders
) t
WHERE rn = 1;
- 注意
ORDER BY必须明确指定字段,不能用ORDER BY id DESC代替时间字段,除非id是自增且严格对应插入顺序 - 若存在多条同时间记录,
ROW_NUMBER()会任意选一个;需稳定结果可用RANK()或加二级排序(如ORDER BY update_time DESC, id DESC) - PostgreSQL / SQL Server / Oracle / MySQL 8.0+ 均支持,但 SQLite 需 3.25+ 且需启用窗口函数编译选项
MySQL 5.7 或旧版本只能靠相关子查询
没有窗口函数时,常见写法是让主表每行去子查询找本组最大时间,再回连匹配:
SELECT o1.* FROM orders o1 WHERE o1.update_time = ( SELECT MAX(o2.update_time) FROM orders o2 WHERE o2.user_id = o1.user_id );
- 这个写法看似简洁,但若
user_id + update_time不唯一,可能返回多行——比如两人同秒下单,都会被选中 - 性能隐患大:子查询对主表每行执行一次,无索引时是 O(n²);务必确保
(user_id, update_time)有联合索引 - 如果只要“最新一条”,但允许任意一条(不关心并列),可用
LIMIT 1配合ORDER BY的相关子查询,但语法更绕,可读性差
GROUP BY 配合 MAX() 只能取部分字段,别硬凑完整行
有人试图这样写:SELECT user_id, MAX(update_time), status FROM orders GROUP BY user_id,这是危险的——status 值来自哪一行完全不确定,MySQL 5.7 严格模式直接报错,其他数据库可能返回随机值。
-
MAX(update_time)是确定的,但同行的status、id等非聚合字段不可信 - 即使测试时看起来“对”,换数据、换版本、换优化器路径都可能崩,属于未定义行为
- 真要兼容老环境又不想写子查询,可先用
GROUP BY拿出user_id和MAX(update_time),再用JOIN回原表匹配,但本质上还是两次扫描
用 LEFT JOIN 自连接筛“不存在更新记录”的方式易错
思路是:如果某条记录是某 user_id 下最新的,那就不该存在另一条同 user_id 且 update_time 更大的记录。于是写:
SELECT o1.* FROM orders o1 LEFT JOIN orders o2 ON o1.user_id = o2.user_id AND o1.update_time
- 逻辑正确,但极易漏掉
NULL边界:若update_time允许为NULL,所有NULL行都会被误判为“最新”,因为o1.update_time 永远为FALSE - 索引效率不如窗口函数或带索引的子查询,
o1.update_time 条件难以走索引范围扫描 - 语义绕,维护成本高,新人看不懂为什么
IS NULL就代表最新
真正关键的不是语法多花哨,而是想清楚:你是否接受并列最新?要不要绝对稳定?目标数据库版本能否支持窗口函数?这三个问题没理清,后面所有写法都在碰运气。










