ROW_NUMBER()必须配合PARTITION BY分组和ORDER BY排序才能取每组最新记录;需嵌套子查询或CTE筛选rn=1,且排序字段应建联合索引以保障性能。

用 ROW_NUMBER() 配合 PARTITION BY 是最通用解法
直接在子查询或 CTE 中对每组按时间/ID升序编号,取 rn = 1 的行。几乎所有主流 SQL 引擎(PostgreSQL、SQL Server、Oracle、MySQL 8.0+)都支持。
常见错误是写成 WHERE rn = 1 却没套子查询——ROW_NUMBER() 是窗口函数,不能直接在 WHERE 中引用;也有人误用 GROUP BY + MIN(time),结果只拿到时间值,拿不到对应整行数据。
- 必须用子查询或 CTE 包裹窗口函数结果,再过滤
rn = 1 -
PARTITION BY列要和业务分组逻辑一致(比如user_id、order_id) -
ORDER BY必须明确指定排序依据,且含唯一性字段(如加id防止并列):ORDER BY created_at, id
WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY created_at, id
) AS rn
FROM orders
)
SELECT id, user_id, created_at, amount
FROM ranked
WHERE rn = 1;MySQL 5.7 或 SQLite 等不支持窗口函数时怎么办
得靠关联子查询或左连接模拟“最小值所在行”。性能较差,但兼容性好。
典型错误是写成 WHERE created_at = (SELECT MIN(created_at) ...) —— 如果同一组有多个记录时间相同,会返回多行,不是“最早一条”而是“最早一批”。
- 用
LEFT JOIN自连接,匹配更早的同组记录;若无匹配,则当前行为最早 - 或用相关子查询,检查是否存在同组中更小的
created_at(或更小的created_at, id组合) - 务必在
GROUP BY字段和排序字段上建联合索引,否则全表扫描很慢
SELECT o1.*
FROM orders o1
LEFT JOIN orders o2
ON o1.user_id = o2.user_id
AND (o2.created_at < o1.created_at
OR (o2.created_at = o1.created_at AND o2.id < o1.id))
WHERE o2.id IS NULL;
JOIN 写法里最容易忽略的去重陷阱
很多人以为 JOIN 只是关联,其实如果驱动表某组有多条记录,而被驱动表只有一条匹配,仍可能因中间临时结果膨胀导致重复——尤其在用 MIN(id) 关联时。
例如:先聚合出每组最小 id,再 JOIN 原表。但如果原表有两条记录 id=100 和 id=101,都属于同一组,且 MIN(id)=100,那 JOIN 后只返回 id=100 行——这没问题;但若你误把 created_at 当作唯一键去 JOIN,而该时间有重复,就可能漏掉或重复。
- 永远用主键或带唯一约束的字段做最终关联依据(推荐
id) - 避免用非唯一字段(如
created_at、status)作为JOIN条件的右侧值 - 执行前用
EXPLAIN看是否出现Using temporary; Using filesort,那是性能警报
PostgreSQL 中 DISTINCT ON 更简洁但仅限它家
这是 PostgreSQL 特有语法,比 ROW_NUMBER() 少写几行,语义更直白:按分组字段去重,保留每组按排序字段最先出现的行。
但它不标准,迁移到其他数据库会直接报错;而且 DISTINCT ON 后的排序字段必须出现在 SELECT 列表最前面,顺序敏感。
- 写法:
SELECT DISTINCT ON (user_id) * FROM orders ORDER BY user_id, created_at, id - 必须跟
ORDER BY,且首字段必须是DISTINCT ON的列 - 如果只写
ORDER BY user_id, created_at,而created_at有重复,结果不确定——PostgreSQL 取哪一行由内部实现决定,不可靠
跨数据库项目里,宁愿多写两行 ROW_NUMBER(),也别绑死 DISTINCT ON。

















