最稳妥方案是用row_number()配合partition by和order by降序取rn=1,适用于postgresql、sql server、mysql 8.0+等;group by+min()易导致字段错配,不可靠。

用 ROW_NUMBER() 配合窗口函数最稳妥
直接在分组内按时间排序并取序号为 1 的记录,是目前主流数据库(PostgreSQL、SQL Server、MySQL 8.0+、Oracle)里最可靠的方式。它不依赖唯一键,也不怕时间字段重复。
常见错误是写成 GROUP BY + MIN(time_col) 再连表——这在多列需要返回时极易出错,尤其当 time_col 不唯一时,MIN() 能取到,但其他字段可能来自不同行。
SELECT id, user_id, created_at, status
FROM (
SELECT id, user_id, created_at, status,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at ASC) AS rn
FROM orders
) t
WHERE rn = 1;
-
ORDER BY created_at ASC确保取“最早”,别漏掉ASC(默认虽是升序,但显式写出更安全) - 如果
created_at可能为NULL,加NULLS FIRST(PostgreSQL)或提前WHERE created_at IS NOT NULL - 多个字段排序时,例如
ORDER BY created_at ASC, id ASC,可消除并列情况下的不确定性
MySQL 5.7 或 SQLite 等不支持窗口函数怎么办?
只能靠关联子查询或左连接模拟“每组最小值”的逻辑,性能较差,且必须确保比较字段组合能唯一标识一行(否则可能漏行或多行)。
典型写法是:对每条记录,检查同组中是否存在更早的记录;若不存在,就是最早的。
SELECT o1.id, o1.user_id, o1.created_at, o1.status FROM orders o1 LEFT JOIN orders o2 ON o1.user_id = o2.user_id AND o1.created_at > o2.created_at WHERE o2.id IS NULL;
- 注意是
o1.created_at > o2.created_at,不是>=,否则相同时间会互相匹配失败 - 如果
created_at有重复,且你希望只取其中一条,需额外加限制(比如最小id):AND (o1.created_at > o2.created_at OR (o1.created_at = o2.created_at AND o1.id > o2.id)) - 该写法在大数据量下容易慢,索引要覆盖
(user_id, created_at)
用 GROUP BY + MIN() 为什么经常翻车?
因为 SQL 标准不允许 SELECT * 同时 GROUP BY 非聚合字段,MySQL 5.7 严格模式下会直接报错:Expression #1 of SELECT list is not in GROUP BY clause。
即使关掉严格模式,执行结果也**不可靠**:MySQL 会随机返回某一行的非聚合字段值,不是和 MIN(created_at) 同一行的数据。
-- ❌ 危险!结果不确定 SELECT user_id, MIN(created_at), status FROM orders GROUP BY user_id;
-
status值可能来自任意一条同组记录,大概率不是最早那条的status - 有些同学试图用
ANY_VALUE(status)掩盖问题,但这只是让报错消失,逻辑依然错 - 真要用
GROUP BY,必须所有非聚合字段都参与分组,或全部套进聚合函数(如MAX(status)),但语义已变
PostgreSQL 里 DISTINCT ON 是个快捷替代
这是 PostgreSQL 特有语法,比窗口函数写起来短,语义清晰,但仅限 PG 使用,别在迁移或兼容性场景下硬套。
SELECT DISTINCT ON (user_id)
id, user_id, created_at, status
FROM orders
ORDER BY user_id, created_at ASC;
-
DISTINCT ON必须配合ORDER BY,且ORDER BY开头字段要和DISTINCT ON一致 - 如果想取“最新”而非“最早”,把
created_at ASC换成DESC即可 - 不能跨数据库移植,换到 MySQL 或 SQL Server 就失效
实际业务里最容易被忽略的,是时间字段精度和时区。比如 created_at 是 TIMESTAMP WITHOUT TIME ZONE,但应用写入时没统一时区,会导致“最早”判断跨天偏移。查之前先确认数据里的时间是否真正可比。











