最直接可靠的方式是用row_number()配合over(partition by user_id order by created_at asc),外层where rn = 1筛选;mysql 5.7不支持窗口函数,需改用join子查询或自连接模拟。

用 ROW_NUMBER() 窗口函数标记每组第一条
需要按某个字段(比如 user_id)分组,并取每组中按时间(比如 created_at)最早的一条记录,最直接可靠的方式是用 ROW_NUMBER() 配合 OVER (PARTITION BY ... ORDER BY ...)。它会给每组内的行严格编号,序号为 1 的就是首次出现的那条。
-
ORDER BY必须明确指定排序依据,否则“首次”无定义;常用时间字段或自增 ID,注意 NULL 值会排在最前(除非加NULLS LAST,PostgreSQL/Oracle 支持,MySQL 8.0+ 不支持) -
PARTITION BY后跟分组字段,多个字段用逗号分隔,如PARTITION BY user_id, status - MySQL 5.7 及更早不支持窗口函数,必须换 JOIN 或子查询方案
SELECT user_id, order_id, created_at
FROM (
SELECT user_id, order_id, created_at,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY created_at ASC, id ASC
) AS rn
FROM orders
) t
WHERE rn = 1;
GROUP BY + 子查询容易漏数据
有人会先用 GROUP BY 拿出每组最小时间,再连回原表匹配,但这种写法隐含风险:
如果同一组存在多条记录时间相同(比如秒级精度下并发插入),
JOIN可能返回多行,或因ON条件不唯一而随机命中某一条若只选
MIN(created_at)而不带其他字段,在 SELECT 列表里直接写order_id会触发 SQL 标准错误(MySQL 5.7+ 默认开启ONLY_FULL_GROUP_BY)更安全的做法是把子查询结果作为过滤条件,而不是连接依据
-
或改用
IN+ 多列元组(需数据库支持,如 PostgreSQL、MySQL 8.0+):SELECT user_id, order_id, created_at FROM orders o1 WHERE (user_id, created_at) IN ( SELECT user_id, MIN(created_at) FROM orders GROUP BY user_id );
MySQL 5.7 或 SQLite 怎么办
这些环境不支持窗口函数,得靠相关子查询或自连接:
-
自连接方式:找不出比自己更早同组记录的行
SELECT o1.user_id, o1.order_id, o1.created_at FROM orders o1 LEFT JOIN orders o2 ON o1.user_id = o2.user_id AND o2.created_at
缺点:数据量大时性能明显下降(O(n²)),且无法处理
created_at完全相等的情况(会返回全部并列记录)-
相关子查询更可控,但语法稍冗长:
SELECT user_id, order_id, created_at FROM orders o1 WHERE created_at = ( SELECT MIN(created_at) FROM orders o2 WHERE o2.user_id = o1.user_id );
注意:若存在时间相同多条,仍会返回全部;要强制只取一条,可在子查询里加
ORDER BY id LIMIT 1(MySQL)或FETCH FIRST 1 ROW ONLY(PostgreSQL),但主查询无法直接复用该排序逻辑,通常得嵌套一层。
ORDER BY 中多个字段怎么定优先级
“首次出现”依赖排序稳定性。如果仅靠 created_at 不足以唯一确定顺序(比如批量导入、日志打点精度不足),必须补充第二排序字段:
推荐补上主键(如
id),它天然有序且非空,能保证每组结果确定避免用可能为 NULL 或重复的字段(如
updated_at、status)作次级排序如果业务上允许“任意一条最早时间的记录”,那单字段足够;但只要后续要做幂等处理、审计或对比,就必须让结果可重现
MySQL 8.0+ 和 PostgreSQL 支持
NULLS FIRST / LAST,但 SQLite 和旧版 MySQL 不支持,遇到 NULL 时间得提前用COALESCE(created_at, '9999-12-31')处理实际上线前务必用真实数据验证:是否存在时间相同、ID 乱序、或分区键为空的情况——这些都会让“首次”偏离预期
窗口函数本身不难,难的是想清楚“首次”的业务定义是否真的只看时间。很多线上问题其实出在假设和现实的偏差上,比如日志延迟、时钟不同步、或批量插入没用事务包住。










