mysql 8.0之前不支持first_value因其依赖窗口函数,而该功能直至8.0才引入;旧版本执行会报错function does not exist。

为什么FIRST_VALUE在MySQL 8.0之前根本用不了
因为FIRST_VALUE是窗口函数,而MySQL直到8.0版本才正式支持窗口函数。如果你用的是5.7或更早版本,执行SELECT FIRST_VALUE(...) OVER (...)会直接报错FUNCTION xxx.FIRST_VALUE does not exist。PostgreSQL、SQL Server、Oracle和SQLite 3.25+都支持,但行为细节有差异——比如PostgreSQL默认窗口帧是UNBOUNDED PRECEDING TO CURRENT ROW,而你想要“分组后第一条”,必须显式写ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,否则可能拿不到真正的首行。
怎么写才能确保取到每个分组的真正第一条
关键不在FIRST_VALUE本身,而在ORDER BY子句和窗口范围。如果只写OVER (PARTITION BY category ORDER BY created_at),它取的是按created_at升序后的第一行值;但如果你本意是取插入顺序的第一条(比如自增id最小的那条),就必须按id排序。常见错误是误用业务时间字段(如updated_at)导致结果漂移。
- 必须明确指定
ORDER BY字段,且该字段在分组内应能唯一确定“第一条”的语义 - 推荐加
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,避免因默认帧范围导致同组内其他行影响结果 - 如果存在并列(如多个记录
created_at完全相同),需叠加次级排序,例如ORDER BY created_at, id
示例(取每类价格最高的商品名称):
SELECT category,
FIRST_VALUE(name) OVER (
PARTITION BY category
ORDER BY price DESC, id
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS top_name
FROM products;
替代方案:当数据库不支持窗口函数时怎么硬解
在MySQL 5.7或旧版PostgreSQL中,得绕开FIRST_VALUE。常见做法是用关联子查询或JOIN + 聚合。但要注意:子查询若没加LIMIT 1且没处理并列,可能返回多行报错;用GROUP BY + MIN(id)再连回原表,性能随数据量下降明显。
- 安全写法是先用子查询找出每组的锚点ID:
SELECT category, MIN(id) AS first_id FROM products GROUP BY category,再JOIN原表 - 不要用
SELECT * FROM (...) t1 WHERE t1.id = (SELECT id FROM ... WHERE category = t1.category ORDER BY xxx LIMIT 1)——相关子查询在大数据量下极慢 - SQLite用户可考虑
ROW_NUMBER() OVER (...) = 1替代,但需确认版本≥3.25且启用了窗口函数编译选项
容易被忽略的NULL陷阱和类型隐式转换
FIRST_VALUE遇到全为NULL的分组时,返回NULL而非跳过——这点和MIN()不同,后者在空集上也可能返回NULL,但语义更清晰。更大的坑是类型:如果FIRST_VALUE(price)作用于DECIMAL字段,但窗口里混入了NULL或隐式转成FLOAT的计算值,结果精度可能丢失。
- 对字符串字段,注意排序规则(collation)是否一致,否则
'apple'和'Apple'可能因大小写敏感性被错排 - 在SQL Server中,
FIRST_VALUE对datetime2字段返回值会保留全部精度,但显示时工具可能截断,别误判为函数问题 - PostgreSQL里若
ORDER BY字段含NULLS FIRST,而你没显式声明,默认是NULLS LAST,会导致NULL值永远拿不到“第一”位置
真正麻烦的从来不是语法会不会写,而是你认定的“第一条”,在别人眼里是不是同一行——时间字段精度、时区、索引缺失导致排序不稳定,都会让结果在不同执行中悄悄变化。










