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

用 ROW_NUMBER() 窗口函数取每组最新一条最稳
直接在 ORDER BY 里按时间倒序,再用 ROW_NUMBER() 标序号,筛出序号为 1 的行即可。关键点是:必须用 created_at 或 updated_at 这类明确的时间字段排序,不能只靠 id——万一插入不是严格递增(比如批量导入、分库 ID 冲突),结果就不可靠。
常见错误现象:ROW_NUMBER() OVER (PARTITION BY category ORDER BY id DESC) 在某些业务场景下会漏掉真正最新的记录。
实操建议:
- 优先使用带时区的
TIMESTAMP WITH TIME ZONE字段,避免因服务器时区不一致导致排序错乱 - 如果时间字段允许 NULL,加
NULLS LAST(PostgreSQL)或先过滤(MySQL/SQL Server) - 示例语句:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY updated_at DESC) AS rn FROM products ) t WHERE rn = 1;
关联子查询在 MySQL 5.7 或无窗口函数环境里还能用
原理是对外层每条记录,查出同分类中时间最大的那个 updated_at,再让外层记录的时间等于它。性能差是硬伤——数据量一过万,全表扫描+多次子查询很容易拖垮查询。
使用场景:老版本 MySQL、某些嵌入式数据库、或临时救急且数据量小于 5000 行。
实操建议:
- 务必给
(category, updated_at)建联合索引,否则子查询几乎必慢 - 避免在子查询里用
MAX(id)代替时间字段,逻辑上不等价 - 示例语句:
SELECT p1.* FROM products p1 WHERE p1.updated_at = ( SELECT MAX(p2.updated_at) FROM products p2 WHERE p2.category = p1.category );
注意 DISTINCT ON 在 PostgreSQL 里更简洁但不跨平台
DISTINCT ON 是 PostgreSQL 特有语法,写起来短,语义也清晰:按 category 分组,每组取 ORDER BY 后第一条。但它在 MySQL、SQL Server、SQLite 里完全不识别,粘贴即报错。
参数差异:DISTINCT ON 后的字段必须出现在 ORDER BY 开头,否则报错;而窗口函数没这限制。
实操建议:
- 仅限确认目标环境是 PostgreSQL 时使用,CI/CD 流水线里要显式标注 DB 版本
- 不能替代窗口函数做“取每组前 N 条”,它只支持前 1 条
- 示例语句:
SELECT DISTINCT ON (category) * FROM products ORDER BY category, updated_at DESC;
时间相同时怎么选?业务规则得提前定清楚
多个记录 updated_at 完全一样很常见(比如批量更新、事务内多行同时改)。窗口函数和 DISTINCT ON 都可能返回任意一条,数据库不保证稳定顺序。关联子查询则可能返回多条,触发“返回多于一行”的错误(PostgreSQL)或静默截断(MySQL)。
容易踩的坑:
- 没加二级排序(如再按
id DESC)就上线,导致相同时间下每次查结果不一致 - 前端分页依赖“最新”记录唯一性,但数据库实际返回了两条并只取了第一条,造成数据丢失感
- 测试用例只覆盖单条更新场景,漏测并发更新下的时间碰撞
真正麻烦的从来不是语法怎么写,而是当两条记录时间戳毫秒级一致时,你有没有和产品对齐过:“这时该留 ID 大的,还是创建时间早的,还是按某种业务权重?”——这个决策不会出现在 SQL 里,但必须出现在需求文档里。










