news 2026/9/14 4:36:48

SQL排名实战:DENSE_RANK窗口函数与并列排名解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL排名实战:DENSE_RANK窗口函数与并列排名解析

1. 题目核心:成绩表里排座次,难点不在SQL而在“并列怎么办”

1.1 题目给了什么:一个极简的分数表

这道题我印象很深,LeetCode编号178,标题就叫“分数排名”。题面极简:一张Scores表,只有两个字段,Id(自增主键)和Score(分数)。要求对分数做排名,规则是:按分数从高到低排,如果两个分数相同,它们名次相同,且下一名的名次要连续,不能出现跳号。

举个例子,原始数据是这样的:

IdScore
13.50
23.65
34.00
43.85
54.00
63.65

期望输出的结果是:

ScoreRank
4.001
4.001
3.852
3.653
3.653
3.504

注意看细节:两个4.00并列第一,接下来3.85直接排第二,而不是第三。两个3.65并列第三,再往下3.50排第四。有没有发现,这里用的是“不跳号的并列排名”。

很多新手第一次做这道题,第一反应是“这不就ORDER BY Score DESC一下就行了吗”——能排出来,但拿不到名次那一列。排序是排序,排名是排名,两者之间差了“如何计算位次”这一步。而并列名次要不要跳号,恰恰是面试官最想确认你懂不懂的细节。

1.2 三个容易被忽略但决定成败的细节

我拿这道题给别人讲的时候,通常会强调三个隐藏考点:

第一,名次从1开始,不是从0开始。这听起来像废话,但写COUNT(DISTINCT ...)ROW_NUMBER()的时候,很多人会忘记处理起始值,导致第一名显示0。

第二,并列必须同名次。两个4.00都是Rank 1,不能一个1一个2。这意味着不能用ROW_NUMBER()直接硬排,因为它给每一行分配绝对唯一的序号,并列也会被拆开。这个函数在这道题里反而是错的。

第三,并列之后名次不跳号。也就是3.85是第2名,不是第3名。这决定了你要用DENSE_RANK()而不是RANK()。这两个函数长得像,行为差异非常关键,后面我会专门展开。

这三个细节如果没想清楚,后面无论用什么解法都会写错。我见过太多人卡在“为什么用RANK不对”“为什么COUNT出来少1”这种问题上,根源就是对这三条规则没有先做拆解。

1.3 为什么这道题成了“必刷经典”

178题能在刷题网站上有这么高的地位,不单是因为题目简单、适合入门。它背后站着一整类真实需求:考试排名、游戏天梯、销售业绩排行、积分榜、商品好评榜……凡是要把“一堆数字”变成“一个有意义的顺序标签”的场景,都会遇到这三个细节。

而且这道题的解法不止一种。窗口函数能做,自连接能做,相关子查询能做,MySQL 5.7时代的@变量也能做。同一个业务语义,在不同技术背景下有完全不同的实现路径,这本身就很有价值。我自己面试候选人的时候也喜欢拿这道题当“试金石”:先问会不会窗口函数,再问知不知道三种排序函数的区别,最后追问一句“如果你用的数据库版本不支持窗口函数怎么办”。这一套下来,SQL水平基本能摸个大概。

所以这篇文章不打算只给一个标准答案。我想把这道题从“背解法”上升到“理解排名逻辑”,从窗口函数到自连接,再到相关子查询,逐个拆给你看。

2. 排序逻辑拆解:RANK、DENSE_RANK、ROW_NUMBER三者到底差在哪

2.1 同一组数据,三种函数给出三种结果

窗口函数是这道题最主流的解法,SQL Server、PostgreSQL、Oracle、MySQL 8.0及以上都支持。但窗口函数里能“产生排名”的至少有三个:ROW_NUMBER()RANK()DENSE_RANK()。新手最容易在它们之间犹豫。

还是上面那张表,三个函数分别跑一遍,结果差异非常清楚:

ScoreROW_NUMBERRANKDENSE_RANK
4.00111
4.00211
3.85332
3.65443
3.65543
3.50664

ROW_NUMBER()不管分数是否相同,强制给每行一个连续且唯一的序号:1、2、3、4、5、6。你完全没法从序号看出“4.00和4.00其实是并列的”。

RANK()识别了并列:两个4.00都是1,但下一个直接跳到3,中间空出2。它遵循的是体育比赛里常见的“赛事排名”逻辑:两个人并列第一,那下一名就是第三名。

DENSE_RANK()也识别并列,但并列之后不跳号:两个1,下一个是2,再下一个是3。这正是178题要求的语义。

一个特别直观的记忆方式:RANK会产生“稀疏排名”,排名数字中间可能有空洞;DENSE_RANK是“稠密排名”,排名数字连续不断。DENSE这个词本身就有“密集、稠密”的含义,可以帮你记一辈子。

2.2 真实业务里怎么选:考试榜、积分榜、TopN各不相同

三个函数没有绝对好坏,只有业务语义不同。我举几个实际例子你就明白了。

考试总分排名通常用DENSE_RANK。两个学生都考了700分,那他们都该是并列第一,下一名是第二。如果用了RANK,第二名就凭空消失了,家长看到成绩单会觉得“第三名上面是谁?为什么第二名没人?”,很难解释。

游戏天梯榜和竞赛排名通常用RANK。竞技类积分讲究“名次差”带来的压迫感。两个人并列第一,第三名的实际位置就是第三名,跳过一个位置反而符合玩家心理预期:前面有两个大佬,我排第三。这种场景下RANK的跳号恰恰是对的。

给每一行生成一个唯一行号用ROW_NUMBER。比如要给订单按时间从新到旧编号,每一行必须是唯一的1、2、3、4,不能出现两个1,因为后续要按行号做分页、抽样或精确更新。这时候哪怕金额相同、时间相同,也必须用ROW_NUMBER强行分出先后。

所以178题为什么选DENSE_RANK而不是RANK?因为题目明确说了“rank should be consecutive”——排名必须是连续的。这类题面措辞就是业务需求文档,读题时多抠一下字眼,能省很多试错时间。

2.3 记住一句话,比背函数签名强

我在带人的时候会让他们记住这句话:ROW_NUMBER管唯一序号,RANK管并列且跳号,DENSE_RANK管并列不跳号。

这句话不需要死记硬背函数说明,而是让你在写SQL前先问一句:业务上,并列存在吗?并列后名次要连续吗?

  • 并列不存在 →ROW_NUMBER
  • 并列存在,且允许跳号 →RANK
  • 并列存在,且不跳号 →DENSE_RANK

178题是第三种。确认好这一点,题已经解了一半。剩下的只是把DENSE_RANK()放进窗口函数里,再ORDER BY一下而已。

3. 第一种解法:DENSE_RANK窗口函数(也是标准答案)

3.1 整条SQL长什么样

直接上代码,这应该是LeetCode 178题最经典的解法:

SELECT Score, DENSE_RANK() OVER (ORDER BY Score DESC) AS Rank FROM Scores;

你没看错,就三行。这也是这道题被很多人称为“送分题”的原因——前提是你知道DENSE_RANK这个函数。

在MySQL 8.0、PostgreSQL、SQL Server、Oracle里,这条SQL都能直接跑。如果面试官没有额外限制,写这个解法是最稳妥的。

3.2 逐段解释:为什么这么写就对了

我把这条SQL拆成三部分看。

SELECT Score没什么好说的,取原始分数。

DENSE_RANK() OVER (...)是核心。OVER关键字声明这是一个窗口函数,括号里没有PARTITION BY,说明不分组,整个结果集当作一个窗口。ORDER BY Score DESC定义窗口内排序规则,分高的排前面。DENSE_RANK()根据这个排序结果计算名次,遇到相同分数给相同名次,且不跳号。

Rank是别名。这里我特意用双引号括起来,是因为RANK在某些数据库里是保留关键字,不加引号可能报错。LeetCode的MySQL执行环境通常能直接写AS Rank,但我建议你养成加引号的习惯:“Rank”。避免了关键字冲突,也显得专业。

3.3 面试爱追问的执行顺序问题

能写出上面这条SQL,只能算“会背”。面试官通常会追问一句:窗口函数的计算发生在SQL执行的哪一步?

这个问题的标准答案是:窗口函数在GROUP BY、HAVING之后,ORDER BY之前计算。如果把一条SQL的执行顺序粗略列出来,大概是:

  1. FROM确定数据源
  2. WHERE过滤原始行
  3. GROUP BY分组
  4. HAVING过滤分组
  5. 窗口函数在这一步之后开始计算
  6. SELECT输出列(窗口函数的结果在这里参与输出)
  7. ORDER BY排序
  8. LIMIT截断

所以在这道题里,先确定要处理的是Scores整张表,没有过滤、没有分组,直接进入窗口函数计算,给每一行算出名次,最后按Score DESC排序输出。

理解了执行顺序,就能解释一个常见疑问:“为什么WHERE里不能直接用Rank = 1来过滤?”因为WHERE发生在窗口函数之前,此时Rank根本还不存在。想要“每个分组排名第一”这种结果,必须得先算出字段,再在外面套一层子查询,这也是后面实战变形里必备的思路。

3.4 变体:如果题目要求变了怎么改

面试官有时候会把题改一改,但只要你会了DENSE_RANK,其他变体也只是换函数而已:

  • 改成允许跳号的并列排名:把DENSE_RANK()换成RANK(),一行改动。
  • 改成唯一连续序号不并列:换成ROW_NUMBER(),一行改动。
  • 要求按分数从低到高排名:把ORDER BY Score DESC改成ORDER BY Score ASC
  • 要求每个班级单独排名:多加一个PARTITION BY ClassId即可。

我把这些变体列成一个表,方便你对照:

业务需求窗口函数写法
并列同名次,名次连续DENSE_RANK() OVER (ORDER BY Score DESC)
并列同名次,名次跳号RANK() OVER (ORDER BY Score DESC)
每行唯一序号,不分并列ROW_NUMBER() OVER (ORDER BY Score DESC)
按班级分组后再排名DENSE_RANK() OVER (PARTITION BY ClassId ORDER BY Score DESC)

你看,这道题本质上是“一个函数记不记得住、一条规则能不能看懂”的事。但很多刷题的人恰恰卡在规则的文字表述上,把“consecutive”忽略了,导致写了个RANK还一脸疑惑地自言自语“为什么输出不对”。

4. 第二种解法:自连接 + COUNT(DISTINCT),没有窗口函数也能做

4.1 核心思路:数一数有多少个比自己高(或相等)的去重分数

窗口函数虽然一行搞定,但它对数据库版本有要求。MySQL 5.7及更早版本不支持窗口函数,很多老系统到现在还在用5.7。更重要的是,面试官为了考察你对SQL本质的理解,会故意限制你:“不要用窗口函数,你怎么做?”

这时候自连接就派上用场了。核心思路非常朴素:一个人的名次,等于“分数高于自己的人数 + 1”。如果有3个人的分数比他高,他就是第4名;如果有0个人比他高,他就是第1名。

但这里有个“并列怎么办”的坑。假设有两个人都是4.00分,要计算一个3.85分的人的名次。比3.85高的分数是4.00,可是有两条4.00的记录。如果直接“数行数”,会数到2,于是名次变成3;但期望结果是2,因为两个4.00是同一个名次,只算“一个更高的分数”就够了。

所以关键不是“有多少行分数高于自己”,而是“有多少个不同的分数高于自己”。必须加DISTINCT去重。这就是COUNT(DISTINCT s2.Score)为什么是这条SQL的灵魂。

4.2 两种常见的自连接写法

第一种写法:自连接之后按每条原始记录分组,统计比自己分数高的“不同分数个数”,再加1。

SELECT s1.Score, (SELECT COUNT(DISTINCT s2.Score) FROM Scores s2 WHERE s2.Score > s1.Score) + 1 AS "Rank" FROM Scores s1 ORDER BY s1.Score DESC;

这里用了相关子查询,严格来说不完全是“自连接”,但对每一行Scores s1都去子查询里找“分数高于自己”的去重分数数量。比如4.00分那条,子查询里没有分数比它高,计数为0,加1得1;3.85那条,子查询里只有4.00一个不同分数,计数为1,加1得2;3.65那条,子查询里有4.00和3.85两个不同分数,计数为2,加1得3。

第二种写法:更“正宗”的自连接,把两张表做笛卡尔连接,再用连接条件过滤。

SELECT s1.Score, COUNT(DISTINCT s2.Score) AS "Rank" FROM Scores s1 INNER JOIN Scores s2 ON s2.Score >= s1.Score GROUP BY s1.Id, s1.Score ORDER BY s1.Score DESC;

重点在于s2.Score >= s1.Score这个连接条件。它是“把每一个分数和所有不低于它的分数配对”。以最高分4.00为例,它只能和自己配对(因为不存在比它更高的分数),DISTINCT s2.Score只有4.00一个值,计数1,名次正好是1。对3.85来说,它能和4.00配对,也能和自己配对,去重后有两个值,名次2。对3.65来说,能配上4.00、3.85和自己,去重后三个值,名次3。一路下来,每个分数直接数“有多少个不同的分数不低于自己”,正好就是自己的名次。

这种写法为什么不用加1?因为连接条件用了>=,把“自己”也算进去了。自己永远占一个不同的分数位,所以计数直接从1开始。

4.3 为什么需要DISTINCT:不去重的后果

我把这个坑单独拿出来说,是因为它真的很隐蔽。还是用4.00分举例,假设有两条4.00的记录,要算一个3.50分的人的名次。

如果不加DISTINCTCOUNT(s2.Score)会同时数到两个4.00,再加上3.85、3.65、3.50自己,一共5行,名次5。但期望名次是4,因为4.00虽然出现两次,只是同一个名次。

加了DISTINCT之后,4.00只算一个不同的值,计数就是4。

这个误区我见过不少,尤其是从ROW_NUMBER思维转过来的人,容易下意识认为“排名就是数多少行比自己靠前”。数据没有重复时这个逻辑没错,一旦出现并列,立刻翻车。所以记住这句话:去重的是“分数值”,不是“记录行”。

4.4 与窗口函数做复杂度对比

从可读性和性能上看,窗口函数都是更优选:它只需要扫描一遍数据,排序完直接出结果;而自连接相当于两张表做连接,数据量大时开销明显上升。在LeetCode那几张玩具表上可能感受不到差距,但你把它放到几百万行的真实订单表上跑一次,执行计划能差出好几倍。

不过自连接解法展现了SQL最底层的集合思维:“结果”不过是通过特定的连接条件,把表和自己组合成一张新表,再做聚合。这种思维方式,在业务中遇到奇奇怪怪的“没有现成函数可用”的老数据库时,是能救命的能力。

5. 第三种解法:相关子查询,理解执行顺序的绝佳素材

5.1 子查询写法长什么样

前面提到的第一种自连接写法,其实就是相关子查询。我再把它单独拎出来,是因为那条SQL太适合用来理解“SQL是如何一行一行处理数据的”。

SELECT s1.Score, (SELECT COUNT(DISTINCT s2.Score) FROM Scores s2 WHERE s2.Score >= s1.Score) AS "Rank" FROM Scores s1 ORDER BY s1.Score DESC;

和上一节的自连接写法对比,这版在SELECT子句里嵌入了一个子查询。子查询返回的是“当前这一行条件下”的一个标量值(一个数字),这个数字就是名次。

以第一行4.00为例:

  • 主查询取到s1.Score = 4.00
  • 子查询执行:SELECT COUNT(DISTINCT s2.Score) FROM Scores s2 WHERE s2.Score >= 4.00
  • 结果只有4.00一个值,计数1
  • 所以Rank = 1

下一行可能又是4.00,子查询照样返回1。再往下一行是3.85,子查询执行WHERE s2.Score >= 3.85,4.00和3.85都被计入,计数2。每一行都重新执行一次子查询,相当于拿着主查询当前行的分数,去全表问一遍“有几个不同的分数达到这个线”。

5.2 MySQL里相关子查询是怎么跑的

很多教材说“相关子查询的性能不好”,但很少解释为什么。实际上,MySQL对相关子查询的处理很大程度上是针对每一行都执行一次子查询。主表有1000行,子查询就可能被触发1000次,每次都扫描完整张表做过滤和聚合。这样一来,总体的扫描量近似是“行数 x 表大小”,数据量一大就很吃亏。

这也是为什么现在生产环境里,能用窗口函数就用窗口函数。窗口函数只需要一次扫描、一次排序就能完成同样的计算,性能差距可能会是数量级的。但如果你的面试官就是要考你手写相关子查询,那重点考察的其实是:你懂不懂“主查询每一行会代入子查询重新算”这件事。

MySQL 8.0在某些场景下会把相关子查询改写成LEFT JOIN或半连接来优化,但是依赖优化器这种变量,远不如自己把SQL写干净来得踏实。

5.3 三个坑:性能、空值、别名

用相关子查询解178题,有三个方面要额外小心。

性能问题最直观。LeetCode的样例表就几行,跑起来毫无感觉。但如果你把同样的逻辑搬到一张100万行的订单表,可能几十秒都出不来。我自己在业务上写报表时,吃过这个亏:一个简单的排名需求,用子查询跑了快一分钟,改成窗口函数后一百毫秒不到。那次之后我形成了一条习惯——能用窗口函数,绝不用相关子查询。

空值问题容易被忽略。如果Score列允许为NULL,COUNT(DISTINCT s2.Score)会忽略NULL值,但WHERE s2.Score >= s1.Score遇到NULL比较时结果是不确定的(NULL >= 某个值,结果是NULL,也就是不满足条件),这可能导致排名结果和业务预期不一致。真实场景里,缺考的0分和缺考的NULL完全是两码事,建议先WHERE Score IS NOT NULL过滤,或在计算前用COALESCE(Score, 0)把NULL归一化。

别名问题。我前面特意用了双引号"Rank",这是因为RANK是很多数据库的保留字。在外层SQL里如果你写AS Rank不带引号,MySQL可能照样报错。刷题平台可能对这类问题比较宽容,但到了真实项目里,你不想在Code Review时被DBA盯上。

6. 从题目到实战:真实报表里的排名需求和变形

6.1 按组排名:PARTITION BY的加入

178题只要求全局排名,但真实业务里,最常见的需求其实是“分组排名”。比如:按班级给每个人排名,按部门给员工销售额排名,按区域给门店销量排名。

这时候只需要给窗口函数加一个PARTITION BY

SELECT DepartmentId, EmployeeName, Salary, DENSE_RANK() OVER (PARTITION BY DepartmentId ORDER BY Salary DESC) AS "Rank" FROM Employees;

PARTITION BY的作用是把数据在窗口函数内部切成多个独立的“小窗口”,每个窗口各自算排名。可以通俗理解成:先按部门把员工分到几个小组里,再在每个小组内按工资从高到低排座次。

这个写法也是处理“每个组TopN”问题的基础。比如我要“每个部门工资最高的前三名”,不能直接在WHERE里写Rank <= 3,因为窗口函数在WHERE之后才计算。标准做法是套一层子查询:

SELECT DepartmentId, EmployeeName, Salary, "Rank" FROM ( SELECT DepartmentId, EmployeeName, Salary, DENSE_RANK() OVER (PARTITION BY DepartmentId ORDER BY Salary DESC) AS "Rank" FROM Employees ) t WHERE t."Rank" <= 3;

这层嵌套几乎是必考题。会写178题,只是掌握了窗口函数的基础用法;会配合PARTITION BY和子查询做TopN,才算把它用进真实报表里。

6.2 分数列存在NULL时怎么处理

排名需求里NULL是个老问题。假设一张成绩表,缺考的同学Score是NULL而不是0,直接ORDER BY Score DESC时,不同数据库对NULL的排序规则不完全一样:MySQL默认把NULL排到最后,Oracle默认NULL排在最前,PostgreSQL默认NULL排在最后但可以改写NULLS FIRST

这就会导致一个尴尬:同一个排名需求,在不同数据库上跑出来的名次不同。

更麻烦的是自连接和子查询里的比较运算。NULL >= 某个数的结果不是TRUE也不是FALSE,而是UNKNOWN,相当于这条记录被连接条件排除掉。可能你原本想统计“所有分数高于自己的人”,但NULL分数的人既不被算作“高于别人”,也不被别人算作“高于自己”,名次直接被扭曲。

我的建议是:建表时如果分数允许为空,就用默认值兜底,比如DEFAULT 0;实在要用NULL表示“缺考”,那计算排名前先过滤掉缺考行,或者COALESCE(Score, 0)把它转成0再排。排名表里出现NULL,往往是统计口径混乱的信号。

6.3 更多的排名派生函数:PERCENT_RANK和CUME_DIST

如果业务不只是要“第几名”,还要看“排在前百分之多少”,可以使用另外两个窗口函数:PERCENT_RANK()CUME_DIST()

PERCENT_RANK()返回的是相对排名,计算公式是(RANK() - 1) / (总行数 - 1)CUME_DIST()返回的是累积分布,表示“有多少比例的行小于等于当前行”。

举个例子,还是178题那张表:

ScoreDENSE_RANKPERCENT_RANKCUME_DIST
4.00100.333333
4.00100.333333
3.8520.40.5
3.6530.60.833333
3.6530.60.833333
3.50411

这两个函数在绩效评级、成绩等级划分(A/B/C/D)、价格区间分析里特别实用。如果你理解了178题的排名语义,再上手这两个函数会非常顺——它们本质上就是在排名基础上再算一次比例。

7. 我在跑题目和写业务SQL时踩过的几个坑

7.1 写窗口函数前先确认数据库版本

我第一次在项目里用DENSE_RANK,兴致勃勃地写好SQL,结果在测试库上一执行,直接报语法错误。查了半天,才发现那套系统用的还是MySQL 5.6,窗口函数是8.0才引入的。

这个坑特别容易踩,因为LeetCode和本地开发环境一般都比较新,窗口函数随便用,但生产环境的数据库版本可能落后好几年。遇到版本不支持的情况,有两条路:一是用变量模拟窗口函数,MySQL 5.7时代最经典的写法是这样:

SET @prev := -1, @rank := 0; SELECT Score, @rank := IF(@prev = Score, @rank, @rank + 1) AS "Rank", @prev := Score FROM Scores ORDER BY Score DESC;

思路是:定义两个用户变量,一个存上一行的分数,一个存当前名次。如果当前行分数等于上一行分数,名次不变;否则名次加1。注意@prev := Score这条赋值语句的顺序,如果在@rank计算之前就把@prev更新了,IF比较的就是当前行自己,永远相等,排名全部变成1。

二是在SQL里通过自连接和相关子查询硬扛,就是前面的解法二和解法三。虽然性能差一点,但在老版本里属于“有且仅有”的正统思路。这两条路都掌握,你才不会在面试或工作中被版本问题卡住。

7.2 排名结果一样,不代表排名逻辑正确

我见过不少人写ROW_NUMBER() OVER (ORDER BY Score DESC)跑178题,一看输出还“挺像那么回事”:4.00排1、4.00排2、3.85排3。这是最危险的“看似正确”。

为什么说危险?因为题目要求两个4.00并列第一,你给它们排成了1和2,语义完全不同。如果把这套逻辑放到竞赛榜单里,等于说选手A和选手B都是最高分,但一个冠军一个亚军,没人能接受。

所以我做这类题有个习惯:拿到输出后不只盯着前几行看,专门去找“并列的数据”,确认它们的排名是否一致。没有重复数据的测试用例,会把错误的排名函数伪装成正确答案。

7.3 排序不稳定导致的“榜单跳动”

这个问题在刷题时完全看不出来,但放到真实业务里非常坑。

假设一个积分榜单,按积分从高到低排名。积分相同的用户有很多,ORDER BY Score DESC只按分数排序,没有指定第二排序键。数据库底层是并行扫描的,两次查询返回的相同积分行顺序可能不一样。你在第一页看到的用户,刷新一下可能和第二页的用户换了位置——明明积分没变,排名却“跳了”。

解决办法很简单:给ORDER BY加一个唯一键作为次级排序,比如ORDER BY Score DESC, Id ASC。这样即使分数相同,行顺序也是确定的,翻页才不会乱。

这个经验是我在跑一个活动榜单接口时发现的。当时用户疯狂投诉“排名乱跳”,排查到最后,不是排名算法错了,而是排序不稳定加上分页逻辑导致的。要是早点想到加唯一键做次级排序,能少加好几天班。

7.4 从“查得对”到“查得快”:索引怎么加

如果一张表很大,排名查询会非常吃性能。窗口函数内部要做排序,自连接更是要反复扫描表。这时候索引的作用就很关键了。

针对178题这种“按分数排序并排名”的需求,给Score字段建索引能显著加速:

CREATE INDEX idx_scores_score ON Scores(Score);

窗口函数执行ORDER BY Score DESC时,可以直接利用索引的有序性,减少排序开销。自连接里的ON s2.Score >= s1.Score也能通过索引快速定位符合条件的记录。

但要注意:索引不是万能的。如果你的查询还要PARTITION BY分组,那可能更适合建组合索引,比如(DepartmentId, Salary)。一般情况下,先跑一下EXPLAIN看看执行计划,确认有没有用到索引,再决定怎么调整。

7.5 排名需求的三段式自查清单

做了这么多道排名题、写了这么多条排名SQL之后,我总结出一个三段式自查清单,每次写完都照着过一遍,能挡掉绝大多数低级错误:

  1. 并列存在吗?如果数据可能有重复分数,确认自己选的是DENSE_RANK还是RANK,是不是业务要的那种跳号语义。
  2. 并列之后名次要连续吗?题目里说“consecutive”就用DENSE_RANK,说“跳过位置”就用RANK,说“每行唯一”就用ROW_NUMBER
  3. 输出顺序确定吗?除了排名用的排序键,再加一个唯一键做次级排序,防止分页或并行执行时顺序漂移。

这三条看起来简单,但每一条都是从真实事故里总结出来的。写SQL这行,很多问题不是语法不会,而是语义没想清楚。178题就是一道特别经典的“语义题”,搞清楚它,后面所有排名需求都会变得特别顺。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 4:34:37

LSTM时间序列预测:空气质量PM2.5预测与Python实战

简介&#xff1a;面向郑州地区空气质量预测的Python源码&#xff0c;主要服务环境数据分析、机器学习实践者以及相关毕业设计课题&#xff0c;用于解决区域空气质量建模与预测问题。压缩包共20个文件、大小仅652KB&#xff0c;覆盖5个XML配置、4个Python核心源码、5个文本说明、…

作者头像 李华
网站建设 2026/9/14 4:33:18

基于Python+tkinter+MySQL的图书管理系统全流程实战解析

简介&#xff1a;面向计算机相关专业毕业设计、课程设计及大作业场景&#xff0c;这是一套基于PythontkinterMySQL的图书管理系统完整项目。项目采用Python编写核心逻辑&#xff0c;借助tkinter构建图形操作界面&#xff0c;通过MySQL完成图书数据的持久化存储&#xff0c;覆盖…

作者头像 李华
网站建设 2026/9/14 4:32:04

Claude Code 配 TaoToken:体验 GLM-5.2 百万级上下文编码

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 4:31:59

2026代码模型横评:火山引擎综合成本直降80%的实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华