直接用group by取不到真正最新状态,因为非聚合字段(如status、updated_at)与max()结果不保证同源,mysql 5.7+开启only_full_group_by时会报错;必须用子查询或窗口函数获取整行最新记录。

为什么直接用 GROUP BY 取不到真正的最新状态
历史版本表通常有 id、version、status、updated_at 等字段,多人/多线程并发写入时,version 可能不严格递增,或存在相同 version 多条记录。此时仅靠 GROUP BY id + MAX(version) 会丢失对应那行的 status 和 updated_at —— 因为 SQL 标准中,非聚合字段在 GROUP BY 后的行为未定义,MySQL 5.7+ 默认拒绝这种写法(ONLY_FULL_GROUP_BY 开启时直接报错:Expression #3 of SELECT list is not in GROUP BY clause)。
真正要的是“每组中 updated_at 最大的那整行”,不是“每组最大的 updated_at 值”。子查询是绕过该限制最直接的方式。
用相关子查询匹配每行是否为本 id 的最新记录
核心思路:对主表每一行,用子查询查出该 id 对应的最大 updated_at,再判断当前行是否等于这个时间点。
- 适用于数据量不大(比如单表
- 必须给
(id, updated_at)建联合索引,否则子查询会全表扫描,性能断崖式下降 - 注意时区和精度:若
updated_at是DATETIME(3),子查询结果也必须同精度比对,否则可能因毫秒截断导致匹配失败
SELECT t1.id, t1.status, t1.updated_at FROM history_table t1 WHERE t1.updated_at = ( SELECT MAX(t2.updated_at) FROM history_table t2 WHERE t2.id = t1.id );
用窗口函数 + 子查询避免自连接(推荐用于大数据量)
当表超过几十万行,相关子查询的 N×N 复杂度会明显拖慢。改用窗口函数先打标,再外层筛选更高效——但需确认数据库版本支持(MySQL 8.0+、PostgreSQL 8.4+、SQL Server 2005+)。
-
ROW_NUMBER() OVER (PARTITION BY id ORDER BY updated_at DESC)比RANK()更稳妥:即使同一id有多条相同updated_at,也只取其中一行(业务上通常可接受) - 不能直接在
WHERE中用窗口函数别名(如WHERE rn = 1),必须嵌套一层子查询或 CTE - 如果需要保留所有并列最新(即多个相同最大
updated_at的记录都算最新),则改用RANK()并过滤rn = 1
SELECT id, status, updated_at
FROM (
SELECT id, status, updated_at,
ROW_NUMBER() OVER (PARTITION BY id ORDER BY updated_at DESC) AS rn
FROM history_table
) ranked
WHERE rn = 1;
遇到 NULL 或软删除标记时子查询怎么处理
真实场景中,status 可能为 NULL,或存在 is_deleted = 1 字段。若直接按 updated_at 取最大,可能捞出已逻辑删除的脏数据。
- 务必在子查询的
WHERE条件里同步过滤,例如子查询中加t2.is_deleted = 0,主查询也得加,否则外层可能命中已被删的行 - 若
updated_at允许为NULL,MAX()会忽略它,导致结果为空——应提前用COALESCE(updated_at, '1970-01-01')或限定WHERE updated_at IS NOT NULL - 某些旧系统用
version字段替代时间戳,此时须确认version是否全局唯一且单调;否则仍建议优先信updated_at
子查询看着简单,但一旦涉及空值、并发更新、索引缺失或软删除,结果就很容易偏离预期——动手前先 EXPLAIN 看执行计划,比反复调逻辑更省时间。











