最稳解法是ROW_NUMBER()配合PARTITION BY分组和ORDER BY时间字段DESC排序再取rn=1;必须用updated_at等时间字段而非id排序,避免ID不递增导致错误,且需处理NULL和时间相同时的二级排序。

用 ROW_NUMBER() 配合窗口函数最稳
直接在主表里按分组和时间排序,给每条记录标序号,再筛出序号为 1 的行——这是目前兼容性好、逻辑清晰、性能可控的主流做法。
常见错误是写成 SELECT * FROM t WHERE id = (SELECT MAX(id) FROM t AS t2 WHERE t2.group_id = t.group_id),看似简洁,但一旦时间字段有重复、或需要多字段联合判断“最新”,立刻失效;而且 MySQL 5.7 及更早版本对相关子查询优化差,容易全表扫描。
- 必须加
PARTITION BY group_column,否则整个结果只排一次序 -
ORDER BY update_time DESC, id DESC建议补上二级排序,避免时间相同时结果不确定 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;SQLite 3.25+ 也支持
SELECT * FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY category ORDER BY created_at DESC, id DESC
) AS rn
FROM products
) t WHERE rn = 1;
MySQL 5.7 或旧版用自连接模拟“每组最大”
当环境受限无法用窗口函数时,得靠左连接 + NULL 检查来实现“找不出比它更新的同组记录”的逻辑,本质是反向验证。
注意:这个写法在数据量大时容易慢,尤其没建对索引;且如果存在两条记录 created_at 完全相同,可能返回多条——它只保证“没有更晚的”,不保证唯一。
- 关联条件必须包含全部分组字段,比如
t1.category = t2.category - 过滤条件要写在
ON里,不是WHERE,否则变成内连接 - 务必在
(category, created_at, id)上建联合索引,顺序不能错
SELECT t1.* FROM products t1
LEFT JOIN products t2
ON t1.category = t2.category
AND (t2.created_at > t1.created_at
OR (t2.created_at = t1.created_at AND t2.id > t1.id))
WHERE t2.id IS NULL;
NOT EXISTS 写法可读性更强但要注意 NULL
语义明确:“不存在同组中更新的记录”,比自连接更容易理解,但新手常栽在 NULL 判断上——只要子查询里涉及可能为 NULL 的字段(比如 created_at 允许为空),> 比较会整体返回 UNKNOWN,导致漏数据。
- 子查询里所有比较字段都得
IS NOT NULL显式排除 - MySQL 对
NOT EXISTS的执行计划有时不如自连接友好,建议 EXPLAIN 对比 - 适合字段非空率高、且团队熟悉 SQL 三值逻辑的场景
SELECT * FROM products t1
WHERE NOT EXISTS (
SELECT 1 FROM products t2
WHERE t2.category = t1.category
AND t2.created_at IS NOT NULL
AND t1.created_at IS NOT NULL
AND (t2.created_at > t1.created_at
OR (t2.created_at = t1.created_at AND t2.id > t1.id))
);
GROUP BY + 聚合函数只适用于简单场景
有人试过 SELECT category, MAX(created_at), other_col FROM products GROUP BY category,但 other_col 的值是随机的(MySQL 5.7 严格模式下直接报错),根本不能保证来自同一行。
除非你只需要时间戳本身,或者 other_col 是确定性聚合(如 MAX(price)),否则这条路走不通。别被“看起来能跑”骗了——数据错得悄无声息。
- PostgreSQL 和 MySQL 8.0+ 的
ANY_VALUE()是妥协方案,但语义模糊,不推荐用于关键业务 - 想省事就用窗口函数,想兼容老环境就老老实实自连接或
NOT EXISTS
真实业务里,“最新”往往隐含业务规则:是最后修改时间?最后入库 ID?还是状态为 “active” 里的最新?这些细节不明确,再漂亮的 SQL 也会查出错误结果。










