先聊一个不少同学踩过的坑:用 GROUP BY 加 MAX(updated_at) 确实能拿到每个分类的最新更新时间戳,但你要的是那条完整记录——时间戳之外的字段全没着落。真正靠谱的解法是窗口函数 ROW_NUMBER() 按分类和更新时间倒序编号,然后只取编号为 1 的行。MySQL 里可以配合 JOIN 子查询,PostgreSQL 则有更简洁的 DISTINCT ON。另外提醒一句:过滤条件放错位置,结果会悄悄出错。

用窗口函数 ROW_NUMBER() 按分类和时间排序取首行
假设表名叫 articles,字段有 category、title、content、updated_at。标准写法长这样:
SELECT category, title, content, updated_at
FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY category
ORDER BY updated_at DESC, id DESC
) AS rn
FROM articles
) ranked
WHERE rn = 1;
这里有两个容易忽略的细节。一是 ORDER BY updated_at DESC 确保最新记录排在最前;二是额外加上 id DESC——如果同一分类下多条记录在同一秒更新,不指定 id 会导致每次查询结果不稳定,加个 id 排序就能避免这种“随机性”。
MySQL 5.7 或更老版本不支持窗口函数怎么办
老版本没有窗口函数,得用关联子查询或自连接,性能差一些,写法也绕。核心思路是:先对每个分类求出最大 updated_at,再连回原表匹配。推荐用 INNER JOIN + 子查询,相对易读:
SELECT a1.* FROM articles a1 INNER JOIN ( SELECT category, MAX(updated_at) AS max_updated FROM articles GROUP BY category ) a2 ON a1.category = a2.category AND a1.updated_at = a2.max_updated;
但有个陷阱:如果同一分类下有多条记录的 updated_at 完全相同,这个写法会返回多条——你拿到的不是“最新一条”,而是“最新时间点的所有记录”。要想严格只取一条,可以在子查询里加 id 辅助去重,或者改用 LEFT JOIN 自连接,找“不存在更新时间更大的同分类记录”。哪种都好,关键是要意识到这个场景。
PostgreSQL 中 DISTINCT ON 是更简洁的替代方案
如果你是 PostgreSQL 用户,有更优雅的选项:
SELECT DISTINCT ON (category)
category, title, content, updated_at
FROM articles
ORDER BY category, updated_at DESC, id DESC;
DISTINCT ON 的用法很直白:对每个 category 只保留一条记录,至于保留哪条,由 ORDER BY 中的后续字段决定。这里 updated_at DESC, id DESC 确保留下最新且 ID 最大的那条。相比窗口函数少一层嵌套,可读性更好。当然,这是 PG 专有语法,跨数据库迁移时要小心。
WHERE 条件不能直接写在窗口函数外层
一个经典错误:比如你想只统计 status = 'published' 的最新记录,结果把 WHERE status = 'published' 放在了最外层。这会导致先取每组最新,再过滤——如果某个分类的最新记录恰好不是 published,这个分类就会消失,而不是退而求其次选次新的 published 记录。逻辑就全歪了。
正确做法是把过滤条件放在子查询或 CTE 内部,确保排序和编号基于已筛选的数据集:
SELECT category, title, content, updated_at
FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY category
ORDER BY updated_at DESC, id DESC
) AS rn
FROM articles
WHERE status = 'published' -- ✅ 在这里过滤
) ranked
WHERE rn = 1;
真正麻烦的不是语法本身,而是想清楚“最新”是针对全量数据还是某个子集。这个逻辑偏差,比写错函数名还容易引发线上问题。写 SQL 时多问自己一句:我要的“最新一条”,是在哪些数据里挑?
