在业务报表中,我们常常需要按某个维度分组,并取出每组内部排名靠前的几条记录。比如按店铺分组取营业额前三的商品,或按班级分组取分数前三的学生。MySQL 8.0 提供了标准的窗口函数,其中 DENSE_RANK() 能够优雅地处理并列名次并连续排名,是完成该需求的推荐方案。

一、什么是 DENSE_RANK() 窗口函数
DENSE_RANK() 是 SQL 标准中的窗口排序函数,它会根据 ORDER BY 子句在分区(PARTITION BY)内为每一行计算一个排名。与 RANK() 不同,DENSE_RANK() 在出现并列值时不会跳过后续名次。例如两个并列第一,下一个仍然是第二,而不是第三。这种特性在“取前三名”且允许并列的场景中非常实用。
从底层逻辑看,窗口函数并不会将多行聚合成一行,而是在保留原表所有行的同时,额外计算出一个排名列。执行时 MySQL 会先按 PARTITION BY 分组,再在组内按 ORDER BY 排序,最后通过滑动计数方式赋予密集排名。由于不改变行数,我们可以直接在外部查询中用 WHERE 条件筛选排名小于等于 3 的记录。
二、准备示例数据
为了直观演示,我们创建一个销售记录表,包含门店编号、商品名称和销售数量三个字段,并插入一些测试数据,其中包含同门店同销量的并列情况。
CREATE TABLE sales (
store_id INT,
product_name VARCHAR(50),
quantity INT
);
INSERT INTO sales (store_id, product_name, quantity) VALUES
(1, 'A', 100),
(1, 'B', 100),
(1, 'C', 90),
(1, 'D', 80),
(2, 'X', 50),
(2, 'Y', 50),
(2, 'Z', 30),
(2, 'W', 20);
上述数据中,门店 1 的 A 和 B 销量都是 100,应当并列第一;门店 2 的 X 和 Y 都是 50,也应并列第一。如果我们希望取出每个门店销量前三名的商品(含并列),DENSE_RANK() 会非常合适。
三、使用 DENSE_RANK() 获取分组前三名
下面通过子查询将 DENSE_RANK() 计算出的排名作为临时列,再在外层过滤出排名不超过 3 的行。注意窗口函数不能直接写在 WHERE 中,因此必须包一层查询。
SELECT
store_id,
product_name,
quantity,
rnk
FROM (
SELECT
store_id,
product_name,
quantity,
DENSE_RANK() OVER (
PARTITION BY store_id
ORDER BY quantity DESC
) AS rnk
FROM sales
) t
WHERE t.rnk <= 3
ORDER BY store_id, rnk;
执行后,门店 1 会返回 A、B(并列第一)、C(第二)、D(第三);门店 2 会返回 X、Y(并列第一)、Z(第二)、W(第三)。如果改用 RANK(),并列第一后下一名会变成第三,从而可能漏掉真正的“第二”,这正是 DENSE_RANK 的优势。
在性能方面,MySQL 8.0 对窗口函数做了优化,通常会使用内存临时表完成分区排序。当数据量较大时,建议为 PARTITION BY 和 ORDER BY 涉及的列建立联合索引,例如本例可建 (store_id, quantity) 索引,以减少排序开销。
四、与变量模拟写法的对比
在 MySQL 5.7 及更早版本没有窗口函数时,开发者常用用户变量模拟排名。这种写法不仅容易出错,而且在并列处理和分组边界上需要写大量 CASE 逻辑。
SELECT store_id, product_name, quantity, rnk
FROM (
SELECT
store_id, product_name, quantity,
@r := IF(@s = store_id, @r + 1, 1) AS rnk,
@s := store_id
FROM sales, (SELECT @s := NULL, @r := 0) v
ORDER BY store_id, quantity DESC
) t
WHERE rnk <= 3;
上面的变量写法只能生成普通连续序号,无法天然处理并列密集排名,若要模拟 DENSE_RANK 还需额外嵌套。而窗口函数版本语义清晰、不易写错,也方便数据库优化器生成更优执行计划。因此在 MySQL 8.0 环境中,应优先使用 DENSE_RANK()。
五、常见注意事项
首先,DENSE_RANK() 计算的是“密集排名”,如果业务要求“最多返回三行”而不是“排名前三”,则应改用 ROW_NUMBER()。其次,ORDER BY 子句建议显式写明排序方向,避免默认升序导致取成倒数前三。最后,在子查询别名和外层过滤时,确保排名列名不与保留字冲突。
当分组内不同记录排序值相同时,DENSE_RANK 会赋予相同名次,这可能导致最终返回行数超过预期的三行。若产品经理想要严格限制行数,需结合业务确认是否允许并列扩张,或改用其他函数。理解这些差异,才能让分组取前 N 名的 SQL 稳定服务于生产报表。
MySQL_8.0DENSE_RANK窗口函数修改时间:2026-08-07 14:09:27