NTH_VALUE(salary, 2)可能返回NULL,因其默认窗口帧为ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,仅向前查找,无法获取排序后位于当前行之后的第二高值;需显式指定全帧范围并确保分组至少有2行。

为什么NTH_VALUE(salary, 2)可能返回NULL?
直接写 NTH_VALUE(salary, 2) OVER (PARTITION BY dept ORDER BY salary DESC) 很容易得到一堆 NULL——这不是bug,而是函数默认的窗口帧(frame)行为导致的。NTH_VALUE只在当前行的窗口帧范围内找值,而默认帧是 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,也就是说它只能往“上”看,无法看到排序后排在自己后面的第二高值。
解决方法是显式指定帧范围:
- 用
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING让整组都参与计算 - 必须配合
ORDER BY(否则顺序未定义,第2个无意义) - 如果某组少于2条记录,
NTH_VALUE仍返回NULL,这是预期行为
如何确保“第二高”不重复计数(即跳过并列)?
如果两个员工薪资都是15000,并列第一,你想要的是真正的“第二高不同值”(比如14500),那 NTH_VALUE 不够用——它按行号取,不是按去重后排名取。
此时应改用 DENSE_RANK() 配合条件过滤:
SELECT dept, salary
FROM (
SELECT dept, salary,
DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rnk
FROM employees
) t
WHERE rnk = 2;
注意:DENSE_RANK 会把并列最高都标为1,下一个不同值标为2;若要用“严格第二行”(不管是否并列),才回到 NTH_VALUE + 全帧。
PostgreSQL、Oracle、SQL Server支持情况差异
NTH_VALUE 在 PostgreSQL 11+、Oracle 11gR2+、SQL Server 2012+ 中可用,但 MySQL **不支持**(截至8.4)。MySQL用户需用变量或 ROW_NUMBER() 模拟:
- MySQL 8.0+ 可用:
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC)然后外层WHERE rn = 2 - 但
ROW_NUMBER()不跳过并列,和NTH_VALUE行为一致;要跳并列就得用DENSE_RANK() - SQLite 目前(3.45)仍不支持
NTH_VALUE
性能提醒:别在大表上无过滤地跑NTH_VALUE
NTH_VALUE(... ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) 要求对每个分组做全量扫描+排序,比只取TOP N的聚合更重。
如果只是想查每个部门第二高薪资(单值),更高效的做法是:
- 先用
GROUP BY dept+ARRAY_AGG(salary ORDER BY salary DESC)(PostgreSQL)取数组,再取下标1 - 或用两次
MAX():先算出最高值max1,再MAX(CASE WHEN salary < max1 THEN salary END) - 窗口函数适合需要随行输出结果的场景(比如每行标出本组第二高),不是单纯求统计值
真正容易被忽略的是帧定义——90%的 NTH_VALUE 返回 NULL 问题,都卡在这一步没显式写 ROWS BETWEEN...。

















