DENSE_RANK()必须配合PARTITION BY store_id使用,否则会全局排序而非分店排名;它对相同业绩赋予相同连续名次(如1,1,2),避免跳号误导决策,且需注意NULL/零值处理与索引优化。

DENSE_RANK() 是计算各门店业绩贡献排名最直接、最不易出错的方案,前提是数据库版本支持窗口函数(MySQL 8.0+、PostgreSQL、SQL Server、Oracle 均可;MySQL 5.7 及更早版本不支持)。
为什么必须用 PARTITION BY store_id
不加 PARTITION BY store_id,DENSE_RANK() 就会把所有门店混在一起排全局名次,完全违背“各门店”这个分组前提。比如北京店和上海店业绩都是 200 万,没分区时它们可能分别排第 3 和第 5;加了分区后,各自在本店维度下排名才真正反映其内部相对位置。
- 正确写法:
DENSE_RANK() OVER (PARTITION BY store_id ORDER BY revenue DESC) - 错误写法:
DENSE_RANK() OVER (ORDER BY revenue DESC)(漏掉PARTITION BY) - 如果表里没有
store_id字段,得先通过 JOIN 或子查询补上,否则分区无依据
ORDER BY revenue DESC 的坑:NULL 和零值怎么排
默认情况下,revenue 为 NULL 或 0 时,在 DESC 排序中多数数据库会把它放在最前面(即名次为 1),这显然不符合业务逻辑——业绩为空或为零的门店不该占高位。
- 显式排除:
WHERE revenue IS NOT NULL AND revenue > 0 - 或用
CASE强制归类:ORDER BY CASE WHEN revenue IS NULL OR revenue = 0 THEN -1 ELSE revenue END DESC - 注意:不同数据库对
NULLS FIRST/LAST的默认行为不一致,靠默认不如主动控制
和 RANK()、ROW_NUMBER() 混用时的名次语义差异
当两家门店业绩同为 150 万元,第三家是 140 万元时:
-
DENSE_RANK()给前两家都是 1,第三家是 2(连续,不跳号) -
RANK()也给前两家都是 1,但第三家是 3(跳过 2) -
ROW_NUMBER()给三家分别是 1、2、3(强制唯一,无视数值是否相同)
绩效/贡献类排名几乎总是要“同业绩同名次”,且后续名次不能跳空——否则“第 3 名”实际对应的是第二档业绩,容易误导运营决策。
性能与索引建议
大表上跑 DENSE_RANK() 很慢,根本原因是排序开销大。关键不是加索引就能解决,而是索引要匹配窗口函数的执行路径:
- 最优索引:
CREATE INDEX idx_store_rev ON sales_data (store_id, revenue DESC); - 只建
revenue单列索引无效——PARTITION BY和ORDER BY是联合驱动排序的 - 如果查询还带时间范围过滤(如
WHERE sale_date >= '2026-01-01'),索引应扩展为(store_id, sale_date, revenue DESC)
真正容易被忽略的是:窗口函数的排序无法利用非前导列的索引。哪怕你有 (revenue, store_id) 索引,只要 PARTITION BY 在前,它就基本没用。

















