MIN(score) OVER()默认计算全窗口最小值,无法实现“截至当前行的累计最低分”;必须显式指定PARTITION BY emp_id、ORDER BY eval_date及ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW才能正确表达“至今”语义。

MIN() OVER 为什么不能直接写成 MIN(score) OVER()
因为 MIN(score) OVER() 默认是 全窗口,会算出整个结果集的最低分,不是“至今”——也就是每个员工从入职到当前行时间点的最低分。要实现“至今”,必须明确指定窗口帧(frame),否则逻辑就错了。
常见错误现象:MIN(score) OVER(PARTITION BY emp_id) 看似分组了,但没定义 ORDER BY 和 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,结果仍是该员工全部记录的最小值,而非随时间累积更新的“至今最低”。
- 必须带
ORDER BY(通常是绩效日期eval_date),否则窗口无法确定“至今”的顺序 - 必须显式写
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,不能省略——某些数据库(如 PostgreSQL)在有ORDER BY时会默认此帧,但 MySQL 8.0+ 和 SQL Server 不会,省略即等价于RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,又变回全窗口 - 分区键
PARTITION BY emp_id是必需的,否则不同员工数据会混算
正确写法:带排序和显式帧的窗口定义
假设表叫 performance,字段含 emp_id、score、eval_date,那么计算每位员工“截至本次评估的最低分”应这样写:
SELECT
emp_id,
score,
eval_date,
MIN(score) OVER (
PARTITION BY emp_id
ORDER BY eval_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS min_score_to_date
FROM performance;
注意:ORDER BY eval_date 决定了“至今”的时间线;如果同员工有多条同日期记录,建议加二级排序(如 ORDER BY eval_date, id)避免非确定性排序。
- MySQL 8.0+、PostgreSQL 11+、SQL Server 2012+ 都支持该语法
- SQLite 3.25+ 支持,但不支持
ROWS子句,只能用RANGE,且对重复日期行为需额外验证 - Oracle 用法相同,但旧版本(
遇到 NULL 值怎么办?
MIN() 窗口函数默认跳过 NULL,这通常符合预期(比如某次评估缺分,不应拉低“至今最低”)。但如果你希望把 NULL 当作极小值参与比较(极少见),得先用 COALESCE(score, -9999) 转换——不过这属于业务逻辑扭曲,一般不推荐。
- 若某员工所有
score都是NULL,则min_score_to_date结果也是NULL - 想过滤掉无有效评分的记录,应在
WHERE或 CTE 中提前处理,而不是靠窗口函数“修复” - 别用
IGNORE NULLS(BigQuery/Oracle 扩展语法),标准 SQL 不支持,跨库迁移会失败
性能和索引建议
这类查询在数据量大时容易慢,因为窗口函数需按 emp_id 分组 + 按 eval_date 排序 + 累积计算。没有合适索引的话,执行计划常出现 WindowAgg 节点配合全表扫描。
- 最佳索引:
CREATE INDEX idx_emp_date_score ON performance(emp_id, eval_date, score); - 注意顺序:
emp_id必须最左,eval_date次之,score放最后——这样才能支撑分区 + 排序 + 聚合三重需求 - 如果只查最近 N 条记录,可先用子查询或 CTE 限制范围,再套窗口函数,避免对历史全量数据重复计算
真正容易被忽略的是:当 eval_date 有大量重复值,且未加唯一性约束或二级排序时,窗口帧的“CURRENT ROW”边界可能不稳定,导致同一语句多次执行结果不一致——尤其在并发写入场景下。务必确保时间字段能唯一标识顺序,或主动补上主键排序。

















