子查询算排名又慢又错,因内层COUNT(*)无法用索引、每次全表扫描,且同分跳名次;应改用DENSE_RANK()等窗口函数,性能好、语义准、支持连续排名。

MySQL 8.0里用子查询算排名为什么又慢又错?
MySQL 8.0之前,很多人靠关联子查询或自连接实现“第几名”逻辑,比如:
SELECT a.name, a.score, (SELECT COUNT(*) FROM scores b WHERE b.score > a.score) + 1 AS rank FROM scores a ORDER BY a.score DESC;这种写法在数据量稍大(比如超5千行)时就明显变慢,而且遇到相同分数时会跳名次(如 1,2,2,4),不是严格连续排名。更关键的是:它无法利用索引加速内层
COUNT(*),每次都要全表扫一遍 b 表。
用 RANK()、DENSE_RANK() 和 ROW_NUMBER() 直接替代
窗口函数是真正解决这个问题的方案,语法简洁、性能好、语义明确。三者区别直接影响排名结果:
-
RANK():同分同名,跳过后续名次(1,2,2,4) -
DENSE_RANK():同分同名,不跳名次(1,2,2,3) -
ROW_NUMBER():强制唯一序号,同分也不同名(1,2,3,4)
例如按分数降序排连续名次:
SELECT name, score, DENSE_RANK() OVER (ORDER BY score DESC) AS rank FROM scores;注意
OVER 子句必须存在,且不能省略括号;排序方向(DESC)不写默认是 ASC,和业务直觉相反,容易漏改。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
WHERE 里不能直接用窗口函数别名
这是新手最常踩的坑:
SELECT *, DENSE_RANK() OVER (ORDER BY score DESC) AS r FROM scores WHERE r <= 3; -- 报错!Unknown column 'r'因为窗口函数在 SQL 执行顺序中晚于
WHERE(执行顺序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY)。想筛选前N名,得套一层子查询或 CTE:WITH ranked AS (SELECT name, score, DENSE_RANK() OVER (ORDER BY score DESC) AS r FROM scores) SELECT name, score FROM ranked WHERE r <= 3;CTE 不仅可读性好,MySQL 8.0 也支持物化优化,比老式派生表更稳。
ORDER BY 和 PARTITION BY 组合影响实际业务含义
单纯按全局排序不够——比如要查“每个班级内按成绩排名”,就得加 PARTITION BY:
SELECT class, name, score, RANK() OVER (PARTITION BY class ORDER BY score DESC) AS rank_in_class FROM students;这里
PARTITION BY class 会让排名在每个班级内独立重置。注意:PARTITION BY 字段必须来自当前查询的 SELECT 列或 FROM 表,不能是计算字段(如 DATE(created_at))除非显式写出;另外,如果 ORDER BY 里没包含足够区分度的列(比如多个学生同分又没加 id),ROW_NUMBER() 的结果可能每次执行不一致——MySQL 不保证无明确排序依据时的物理顺序稳定性。

















