用row_number()比嵌套查询更稳、更易读,因其能通过partition by user_id和order by order_time asc, id asc精确锁定每用户首单,避免时间重复导致的多行问题。

直接用窗口函数比嵌套查询更稳、更易读,尤其当需要“每个用户的首单”时,ROW_NUMBER() 是首选。
为什么不用 WHERE + 子查询找 MIN(order_time)?
常见错误是写成:WHERE order_time = (SELECT MIN(order_time) FROM orders o2 WHERE o2.user_id = o1.user_id)。这在用户有多笔订单时间相同时会返回多行——首单本应唯一,但时间重复就破防了。
真正首单取决于“最早创建且最早录入”的逻辑,数据库通常靠主键或自增 ID 隐含顺序。纯靠 MIN(order_time) 无法打破时间平局。
- 时间字段精度不足(如只到秒)极易触发重复
- 没有索引时子查询会全表扫描,性能随用户数增长明显下降
- MySQL 5.7 或更早版本对相关子查询优化差,可能生成临时表
用 ROW_NUMBER() 精确锁定首单(推荐写法)
按 user_id 分组,用 order_time 升序排序,再用 id(或 created_at)保底去重,确保每组只取 1 行:
SELECT user_id, order_id, order_time, amount
FROM (
SELECT user_id, order_id, order_time, amount,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY order_time ASC, id ASC
) AS rn
FROM orders
) ranked
WHERE rn = 1;
关键点:
-
PARTITION BY user_id是分组前提,漏掉就变成全局排序 -
ORDER BY order_time ASC, id ASC中的id ASC是防重保险,避免时间相同时结果不确定 - PostgreSQL / MySQL 8.0+ / SQL Server 2012+ 均原生支持;SQLite 3.25+ 也支持
兼容老版本 MySQL(5.7 及以下)的替代方案
必须用嵌套查询时,别只比时间,要连带比主键:
SELECT o1.user_id, o1.order_id, o1.order_time, o1.amount
FROM orders o1
WHERE NOT EXISTS (
SELECT 1 FROM orders o2
WHERE o2.user_id = o1.user_id
AND (o2.order_time
<p>这个写法等价于“不存在更早的订单”,逻辑严密但可读性差,且 <code>NOT EXISTS</code> 在大数据量下不如窗口函数快。</p>
- 务必给
(user_id, order_time, id)建联合索引,否则性能灾难 - 不能用
LEFT JOIN ... IS NULL替代,因多对一连接易产生笛卡尔积放大 - 如果表里有软删除标记(如
is_deleted = 0),所有子查询都得同步加条件,漏掉就错
真正难的不是写出语法,而是想清楚“首单”的业务定义:是系统最早记录?支付成功时间?还是用户下单按钮点击时刻?时间字段来源不一致时,ROW_NUMBER() 的 ORDER BY 列必须和业务口径对齐,否则技术再准,结果也是错的。











