FIRST_VALUE()必须配合OVER()子句使用,核心是PARTITION BY user_id ORDER BY order_time;缺ORDER BY会导致错误或不可靠结果;需注意NULL处理、排序确定性及联合索引优化。

FIRST_VALUE() 必须配合窗口定义才能生效
直接写 FIRST_VALUE(order_time) 会报错或返回 NULL,因为这个函数不接受单独使用,必须搭配 OVER() 子句明确分区和排序。核心是:按用户分组(PARTITION BY user_id),再按时间升序(ORDER BY order_time),这样才能锁定“首单”。
常见错误是漏掉 ORDER BY —— 没有排序,数据库根本不知道哪个是“第一”。即使加了 PARTITION BY,缺 ORDER BY 也会导致结果不可靠(尤其在 MySQL 8.0+ 或 PostgreSQL 中可能报错)。
- 必须写成
FIRST_VALUE(order_time) OVER (PARTITION BY user_id ORDER BY order_time) - 如果订单时间有重复,建议加上二级排序,比如
ORDER BY order_time, order_id避免非确定性 - 注意 NULL 值:若某用户所有
order_time都为 NULL,FIRST_VALUE也会返回 NULL
默认是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
很多同学发现首单时间被“复制”到了同一用户的每条记录上,这是对的——FIRST_VALUE() 默认只看当前行及之前(即从分区开头到当前行),而由于我们按时间升序排,首行的值在后续行中仍满足“当前行之前出现过”,所以一直能取到。但如果你只想在首单那条记录上显示时间、其余为 NULL,就得换思路:
- 用
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time) = 1做标记,再用CASE过滤 - 不要强行改
FIRST_VALUE的窗口帧(比如写ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING),它依然会把首值广播到整组 - MySQL 8.0+ 和 PostgreSQL 支持
IGNORE NULLS参数,但对首单场景没用——你本来就要非 NULL 时间
不同数据库对 NULL 处理略有差异
SQL 标准里 FIRST_VALUE 默认包含 NULL,也就是说如果某用户最早一条订单的 order_time 是 NULL,它就会被选作“首单时间”。这通常不是你想要的。
- PostgreSQL 和 SQL Server 默认照常返回 NULL;MySQL 8.0+ 同样如此
- 想跳过 NULL,得先过滤或用
CASE预处理:FIRST_VALUE(CASE WHEN order_time IS NOT NULL THEN order_time END) OVER (...),但注意这会产生 NULL,需配合IGNORE NULLS(仅 PostgreSQL 14+、Oracle、SQL Server 2022+ 支持) - 更稳妥的做法是提前 WHERE 掉
order_time IS NOT NULL,避免脏数据干扰逻辑
性能关键:user_id 和 order_time 上要有联合索引
窗口函数本身不走索引,但 PARTITION BY user_id ORDER BY order_time 这个组合,如果底层没有对应索引,数据库就得对每个用户做全表扫描再排序,大表下会明显变慢。
- 建索引语句示例:
CREATE INDEX idx_user_order_time ON orders(user_id, order_time); - 别只建
user_id单列索引——它无法加速排序部分 - 如果表还常按其他字段查询(比如
status),别盲目加更多列到这个索引里,优先保障窗口场景的效率

















