FIRST_VALUE取每组最小值对应ID常出错,因其不判断最小值而仅取排序首行;遇相同value时结果不确定,须加ROWS BETWEEN子句、ORDER BY value,id及明确NULLS处理,或改用ROW_NUMBER()更可靠。

用 FIRST_VALUE 取每组最小值对应的 ID 为什么常出错?
FIRST_VALUE 本身不判断“最小值”,它只按指定排序取第一个值。如果直接写 FIRST_VALUE(id) OVER (PARTITION BY group_id ORDER BY value),确实能拿到每组中 value 最小的那条记录的 id——但前提是排序稳定、无重复 value。一旦同组内存在相同最小 value,数据库可能任意选一个 id(取决于底层执行顺序或是否加了 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING),结果不可靠。
实操建议:
- 必须显式补全窗口帧:加上
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,否则默认是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,会导致非首行结果为 NULL 或错乱 - 排序字段要足够唯一:在
ORDER BY value后追加id(或主键)作为第二排序条件,避免并列时不确定性,例如ORDER BY value, id - 注意 NULL 处理:如果
value可为空,NULLS FIRST或NULLS LAST要明确写出来,否则不同数据库行为不一致(PostgreSQL 默认NULLS FIRST,MySQL 8.0+ 默认NULLS LAST)
替代方案:用 ROW_NUMBER() 更安全可靠
比起依赖 FIRST_VALUE 的隐式取首行,显式编号再过滤更可控。尤其当业务要求“严格取最小 value 对应的最小 id(或任意一个)”时,ROW_NUMBER() 是首选。
示例(标准写法):
SELECT group_id, id, value
FROM (
SELECT group_id, id, value,
ROW_NUMBER() OVER (PARTITION BY group_id ORDER BY value, id) AS rn
FROM t
) t2
WHERE rn = 1;
关键点:
-
ORDER BY value, id确保排序确定性;若只需任一最小值 ID,可改用ORDER BY value, RANDOM()(PostgreSQL)或ORDER BY value, UUID()(MySQL 8.0.31+)打散 -
ROW_NUMBER()保证每组严格生成 1, 2, 3…,不存在并列问题;而RANK()或DENSE_RANK()在值相同时会跳号或并列,无法用rn = 1精确锁定唯一行 - 该子查询结构在所有主流 SQL 引擎(PostgreSQL / MySQL 8.0+ / SQL Server / Oracle / DuckDB)中行为一致
FIRST_VALUE 真正适合的场景是什么?
它适合“取每组排序后首行的某个字段值,且该字段与排序依据无关”的场景,比如:每组最早订单时间对应客户的会员等级、每组最高销售额对应销售员的入职年份。这时你不是在“找最小值的 ID”,而是在“把首行的某静态属性广播到整组”。
例如,想给每组所有行都标上“本组最小 value 对应的 id”:
SELECT group_id, id, value,
FIRST_VALUE(id) OVER (
PARTITION BY group_id
ORDER BY value, id
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS min_value_id
FROM t;
这种写法合理,但注意:
- 必须写全
ROWS BETWEEN ...,否则默认帧范围会让非首行的min_value_id为 NULL - 如果只要最终结果里每组一行,仍需外层
GROUP BY或去重,不能只靠这个窗口函数“压缩”结果集 - 性能上,比
ROW_NUMBER()+ 过滤稍重,因为要为每行计算并填充该值
MySQL 5.7 或 SQLite 这类不支持窗口函数的老版本怎么办?
没法用 FIRST_VALUE 或 ROW_NUMBER(),得退回到相关子查询或连接方式。虽然语法啰嗦,但逻辑清晰、兼容性强。
典型写法(防多匹配):
SELECT t1.group_id, t1.id, t1.value
FROM t t1
WHERE t1.value = (
SELECT MIN(t2.value)
FROM t t2
WHERE t2.group_id = t1.group_id
)
AND t1.id = (
SELECT MIN(t3.id)
FROM t t3
WHERE t3.group_id = t1.group_id
AND t3.value = t1.value
);
要点:
- 两层子查询:先定最小
value,再在这些行里取最小id,确保结果唯一 - 避免只用一层
WHERE value = (SELECT MIN...),否则遇到多个相同最小值时会返回多行,不符合“每组一行”需求 - 有索引的话,
(group_id, value, id)复合索引能显著加速这两个子查询
真正容易被忽略的是排序确定性和 NULL 处理——哪怕语法跑通,数据里混着 NULL 或未加第二排序键,结果就可能每次都不一样。

















