keep是聚合函数的修饰语,用于在排序后的组内限定聚合范围,不能替代row_number()等窗口函数;它必须紧跟min、max等聚合函数,不产生行号或累计计算,仅返回指定排名组内的聚合结果。

KEEP 不能替代基础窗口函数,它和 ROW_NUMBER()、FIRST_VALUE() 等根本不是同一类东西——前者是聚合函数的修饰语,后者是真正的窗口计算函数。硬套会报错或语义错。
KEEP 是什么,不是什么
KEEP 只能跟在 MIN、MAX、SUM、AVG、COUNT 等聚合函数后面,作用是“在排序后的一组行中,限定聚合范围”。它本身不产生行号、不位移取值、不做累计计算。
常见误用场景:
- 想用
KEEP实现“取每组第一条”,却写了SELECT id, KEEP (DENSE_RANK FIRST ORDER BY ts) FROM t—— 语法错误,KEEP前必须有聚合函数 - 把
FIRST_VALUE(col) OVER (...)直接替换成MIN(col) KEEP (DENSE_RANK FIRST ORDER BY ts) OVER (...)—— 语义不同:前者返回首行col值,后者返回所有首排名行中col的最小值 - 在没
GROUP BY的查询里单独用KEEP—— Oracle 报错ORA-30496: Argument should be a constant or a function of expressions in the GROUP BY clause
什么时候该用 KEEP,而不是 FIRST_VALUE/LAST_VALUE
当你真正需要的是“排序后首组(可能多行)中的聚合结果”,而不是“排序后第一行的原始值”时,KEEP 才有意义。典型场景:
- 查每个部门薪资最高者中年龄最小的那个员工的姓名:
MIN(name) KEEP (DENSE_RANK LAST ORDER BY salary) OVER (PARTITION BY dept_id) - 统计每个用户最后一次登录时间点的所有操作数:
COUNT(*) KEEP (DENSE_RANK LAST ORDER BY login_time) OVER (PARTITION BY user_id) - 取每个产品类别中销量前三名的平均价格:
AVG(price) KEEP (DENSE_RANK FIRST ORDER BY qty DESC) OVER (PARTITION BY category)(注意:需配合RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW或手动截断,否则仍是全量排序)
关键区别:FIRST_VALUE(price) OVER (...) 返回排序后第一行的 price;MIN(price) KEEP (DENSE_RANK FIRST ORDER BY qty DESC) OVER (...) 返回所有并列“销量第一”的行中最小的 price。
为什么 KEEP 在 Oracle 里常比手写子查询快一点
不是因为 KEEP 本身快,而是 Oracle 优化器对它的执行路径识别更成熟:
-
MIN(x) KEEP (DENSE_RANK FIRST ORDER BY y)可能触发 early-aggregation,即在排序过程中一旦确认某批y值已是最小,就提前结束该组扫描 - 而等价的手写方式:
(SELECT MIN(x) FROM t t2 WHERE t2.id = t1.id AND t2.y = (SELECT MIN(y) FROM t t3 WHERE t3.id = t1.id)),容易引发嵌套循环 + 多次索引查找 - 但前提是字段
id和y有联合索引;否则两者都会退化为全表排序,KEEP甚至因解析开销略高
别忘了:DENSE_RANK FIRST 仍会全量排序生成稠密排名,不是只排前 N 行。所谓“快”,只是优化器在特定数据分布下做了裁剪,不是语法承诺。
容易被忽略的兼容性与写法细节
在 Oracle 中用 KEEP,这些细节不注意就会静默出错或性能崩塌:
- 金仓(Kingbase)部分版本要求
DENSE_RANK FIRST中间必须有空格,写成DENSE_RANKFIRST会解析失败 -
COUNT(*) KEEP (...)合法,但COUNT(col) KEEP (...)也合法——它只统计非 NULL 的col,和普通COUNT行为一致 - Oracle 不支持
KEEP与OVER混用在非聚合上下文中,例如不能写ROW_NUMBER() KEEP (...) OVER (...),语法直接拒斥 - 如果
ORDER BY字段含大量重复值,DENSE_RANK FIRST可能命中数百行,此时MIN聚合代价不低,不如先用ROW_NUMBER()限 1 行再关联
最常被跳过的动作:跑之前先看执行计划里有没有 SORT AGGREGATE 或 WINDOW SORT 节点。如果有,说明底层仍在全量排序——KEEP 并未帮你省掉计算,只是让 SQL 更紧凑而已。











