必须先用CASE处理达成率再排序:CASE WHEN target>0 THEN ROUND(actual::DECIMAL/target,4) ELSE NULL END AS achievement_rate,并在RANK()中用PARTITION BY department_id ORDER BY achievement_rate DESC,同时WHERE achievement_rate IS NOT NULL且department_id IS NOT NULL。

直接用 ROW_NUMBER() 或 RANK() 做“达成率排名”大概率出错——因为达成率本身是计算字段,且常含 NULL、0 或负值,不预处理就排序会打乱业务逻辑。
达成率字段必须先处理再参与窗口排序
销售达成率通常是 actual / target,但原始数据里常有目标为 0、实际为 NULL、或目标未填等异常。如果直接写 RANK() OVER (ORDER BY actual/target DESC),数据库会报错或返回意外结果(比如 NULL 被排在最前,而业务上它应被排除)。
- 务必在
SELECT或 CTE 中先用CASE计算干净的达成率列,例如:CASE WHEN target > 0 THEN ROUND(actual::DECIMAL / target, 4) ELSE NULL END AS achievement_rate
- 在
OVER子句中用该别名列排序,而非原始字段相除 - 加
WHERE achievement_rate IS NOT NULL过滤掉无效记录,避免干扰排名连续性
分组内排名必须用 PARTITION BY department_id
“按部门看谁完成得最好”不是全局比,而是每个部门独立拉通。漏写 PARTITION BY 就等于把销售总监和新人放一起排,结果完全失真。
- 错误写法:
RANK() OVER (ORDER BY achievement_rate DESC)→ 全公司一张榜 - 正确写法:
RANK() OVER (PARTITION BY department_id ORDER BY achievement_rate DESC) - 注意:若部门字段本身有空值(
department_id IS NULL),这些行会被归到同一个隐式分组,建议提前WHERE department_id IS NOT NULL - MySQL 8.0+ 和 PostgreSQL 支持该语法;旧版 MySQL(
选 RANK() 还是 DENSE_RANK()?看业务是否允许“并列跳名次”
两个销售员达成率都是 120%,接下来一人是 115%——这时你要决定:第三名是“第3名”还是“第2名”?这决定了函数选择。
- 用
RANK():输出是1, 1, 3,适合强调“差距感”的管理场景(如绩效强制分布) - 用
DENSE_RANK():输出是1, 1, 2,适合需要连续名次做后续筛选的场景(如“取每部门前3名”,用RANK()可能实际返回 4 行) - 别用
ROW_NUMBER():它会给相同达成率分配不同序号(如 1, 2, 3),违背“达成一致即表现一致”的业务前提
真实查询要带过滤条件,不能只靠窗口函数兜底
窗口函数只管排序和编号,不管数据时效性、有效性或权限范围。一个可上线的达成率排名查询,至少还要控制三件事:
- 时间范围:加
WHERE sale_month BETWEEN '2026-01' AND '2026-03',否则历史垃圾数据会污染当前排名 - 状态过滤:排除
status = 'canceled'或is_deleted = true的订单/目标记录 - 权限隔离:若涉及多组织,需通过
AND org_id = ?绑定参数,不能依赖应用层二次过滤 - 性能提醒:在
department_id和sale_month上建联合索引,否则PARTITION BY + ORDER BY在百万级表上会明显变慢
真正难的不是写出 RANK() OVER,而是想清楚“谁应该被排、按什么逻辑排、排完怎么用”。业务规则一旦嵌套进计算字段和过滤条件里,窗口函数只是执行环节的一环,不是银弹。

















