keep子句必须与group by及聚合函数配合使用,不可独立存在;其作用是修饰max/min等函数,依据dense_rank或rank排序逻辑筛选分组内的首/尾行值,但不支持partition by且order by中仅允许原始列名或常量。

KEEP子句必须配合GROUP BY和聚合函数一起用
单独写 KEEP 是语法错误,它不是独立语句,而是 MAX()、MIN()、SUM() 等聚合函数的修饰符。常见误写如 SELECT col1 KEEP (DENSE_RANK FIRST ORDER BY col2) FROM t GROUP BY col3 —— 缺少外层聚合函数,Oracle 会报 ORA-00937: not a single-group group function。
正确写法必须是:MAX(col1) KEEP (DENSE_RANK FIRST ORDER BY col2) 或 MIN(col1) KEEP (DENSE_RANK LAST ORDER BY col2)。注意:这里的 MAX / MIN 实际不参与数值比较,只是“容器函数”,真正起作用的是 KEEP 子句里的排序逻辑。
-
FIRST表示取排序后位置最靠前的值(可能多个并列第一,DENSE_RANK会全部保留) -
LAST表示取排序后位置最靠后的值 - 如果只想要一个确定的首行(哪怕有并列),改用
RANK而非DENSE_RANK,再结合ROWNUM = 1或窗口函数更稳妥
ORDER BY 在 KEEP 里不能引用别名或表达式
在 KEEP (DENSE_RANK FIRST ORDER BY ...) 中,ORDER BY 后只能写原始列名或确定的常量,不能写 SELECT 列表中的别名,也不能直接写复杂表达式(比如 TO_DATE(date_str, 'YYYYMMDD')),否则报 ORA-01747: invalid user.table.column, table.column, or column specification。
解决办法是:把表达式提前放到子查询或 CTE 中计算好,再在外部 KEEP 中引用该列名。
- 错误:
MAX(name) KEEP (DENSE_RANK FIRST ORDER BY LENGTH(name))——LENGTH(name)不被允许 - 正确:
WITH t AS (SELECT name, LENGTH(name) len FROM emp) SELECT MAX(name) KEEP (DENSE_RANK FIRST ORDER BY len) FROM t GROUP BY deptno - 也可以用虚拟列(Oracle 11g+)预先定义,避免重复计算
KEEP 和 ROW_NUMBER() OVER 的性能与语义差异
很多人用 KEEP 是为了替代窗口函数取首行,但二者行为不同:当存在并列(相同排序值)时,KEEP 默认返回所有并列值的聚合结果(如 MAX 取最大值),而 ROW_NUMBER() 强制编号、只返回一行。
例如按 salary 降序取“最高薪员工姓名”,若两人同为最高薪:
– MAX(name) KEEP (DENSE_RANK FIRST ORDER BY salary DESC) 返回其中一个名字(取决于 MAX 的字符比较规则)
– FIRST_VALUE(name) OVER (ORDER BY salary DESC) 也只返回一个(稳定,按出现顺序)
– ROW_NUMBER() OVER (ORDER BY salary DESC) + WHERE rn = 1 才真正可控,且可加二级排序消除不确定性
-
KEEP在大数据量分组场景下通常比子查询 +ROWNUM更快,但不如原生窗口函数灵活 - 如果业务要求“任意一个首行即可”,
KEEP简洁;如果要求“按某规则唯一确定”,优先选ROW_NUMBER()或FIRST_VALUE() - 注意
KEEP不支持PARTITION BY,只能配合GROUP BY使用
常见错误:混淆 DENSE_RANK 和 RANK 导致多返回
写 KEEP (DENSE_RANK FIRST ORDER BY x) 时,若分组内有多个 x 值相同且为最小/最大,DENSE_RANK 会给它们同一排名,KEEP 就会把这些行全纳入“首行候选”,最终由外层聚合函数(如 MAX)决定输出——看起来像“随机”返回,实则是聚合结果。
想严格只取一条,有两个办法:
- 换用
RANK:它对并列值跳过后续排名(如两个第一,则下一个为第三),但依然可能多返回(只要并列就都算 rank=1) - 加二级排序:如
ORDER BY salary DESC, emp_id ASC,让排序唯一化 - 最保险的是不用
KEEP,改用ROW_NUMBER() OVER (PARTITION BY deptno ORDER BY salary DESC, emp_id ASC)过滤rn = 1
实际用的时候,先确认业务是否真能接受“多个并列时取聚合结果”,还是必须单条记录——这点容易被忽略,一上线才发现数据偶尔不对。











