☰
SQL第N高薪水查询:从LIMIT到窗口函数的通用解法
2026/10/7 3:41:14 网站建设 项目流程

提到“第二高薪水”,只要是写过SQL的人,十有八九都见过这道题:一张员工表,一个工资字段,让你把第二高的那个数取出来。LeetCode上有、牛客上有、各种公司面试题里也有。很多人背了一个LIMIT 1 OFFSET 1的答案就去应付了,可面试官一旦把“第二”改成“第五”“第八”,或者改成分组取每组第二高,立刻卡壳。

我这些年刷题加实际做报表,最大的体会是:第二高从来不是重点,“第N高”才是真正的通用能力。所谓通用,就是你拿到任何一个N,都能一套思路写到底,而不是每次换个数就现推一遍。这篇文章就把我在MySQL、SQL Server、Hive上折腾过的几种解法全摊开讲,包括它们各自的原理、适用场景、性能差异,以及那些写在注释里都嫌丢人的坑。

这文适合三类人看:正在备战面试的后端/数据岗同学、日常要写报表但只会一种解法的数据分析师,以及想把自己写的SQL从“能跑”提升到“讲得清楚”的开发。我会把每个方案的“为什么这么写”也讲透,你看完之后再遇到第N高、组内第N高、倒数第N高这类变体,应该都能在一分钟内给出靠谱方案。

1. 为什么“第二高”是经典题,真正的考点却是“第N高”

1.1 第二高薪水这道题到底在考什么

原题一般长这样:有一张Employee表,字段是id、name、salary,让你查出第二高的薪水。如果不存在第二高,返回null。它表面上考的是SQL语法,实际上考的是三件事。

第一,你是否理解“排序后取位置”的本质。所谓第二高,就是把薪水从大到小排个序,取第2条记录。理解了这一点,第二高和第八高没有任何区别,只是偏移量变了。第二,你是否知道薪水有重复值。如果两个人都拿9200,那9200是第一还是第二?很多没踩过坑的人会直接写SELECT salary FROM employee ORDER BY salary DESC LIMIT 1,1,然后自信满满地提交,结果被测试用例里的重复数据打脸。这是因为这条SQL没有去重,排序后第二条可能是另一个9200,而不是真正的第二高。第三,你是否能在边界情况下给出正确结果。全表只有一行记录、所有人薪水都一样、N大于总人数,这些情况都要返回null,不能报错也不能返回空结果集让前端解析失败。

所以这道题考的不是你会不会写ORDER BY,而是你面对一个需求时,有没有先想清楚数据约束、再去选解法的习惯。我说的“数据约束”,包括是否去重、是否允许空值、排序时NULL排哪边、数据库版本支不支持窗口函数,这都属于需求边界的范畴。很多人面试栽跟头,就是栽在没问这些。

1.2 从第二高到第N高——通用化的三个层次

我理解的“通用化”有三个层次,你可以拿来自测一下现在在第几层。

第一层:会改数字。把第二高改成第三高时,知道把LIMIT 1,1改成LIMIT 2,1。这不算通用化,这只是换了个参数。

第二层:会写通用查询。无论N是多少,都能用一套固定的SQL模板得出结果,不会因为N变化而重写逻辑。比如窗口函数写法里,N是WHERE rnk = N里的一个变量;计数写法里,N是HAVING cnt = N里的一个参数。这才是真正意义上的通用解法。

第三层:会迁移场景。把“全表第N高”扩展成“每个部门第N高”、把“第N高”扩展成“倒数第N高”、把单字段扩展成多字段联合排序。这三个层次的差异,本质上是抽象能力的差异。刚入门时停留在第一层很正常,但如果你工作两三年还在靠背LIMIT答案混日子,那遇到复杂报表需求就会非常被动。我在实际业务里就遇到过:运营要“每个品类销量前3的SKU”,这就是典型的“分组Top N”,和第N高薪水是同一个家族的问题。你要是只会全表排序取一条,那面对这种需求就只能写存储过程循环,又慢又难看。

所以这篇文章的核心思想就一句话:与其背第二高的答案,不如掌握一套能应对N变化、能迁移到分组场景的通用解法。

2. 通用解法核心拆解:三种主流方案的原理与选择

2.1 方案一:LIMIT/OFFSET定位法,理解排序偏移的本质

先看最简单也最容易被面试官追问的方案。在MySQL里,取第二高可以写:

SELECT DISTINCT salary FROM employee ORDER BY salary DESC LIMIT 1 OFFSET 1;

解释一下这行SQL干了什么:先按salary DESC排序,OFFSET 1表示跳过第1条记录,LIMIT 1表示从第2条开始只取1条。DISTINCT是必须加的,否则重复薪水会把排名位置挤掉。比如两条9200排在第1和第2位,真正的第二高8500排在第3位,你只跳过1条的话取到的还是9200,这就错了。

如果要写成取第N高的通用形式,公式是LIMIT N-1 OFFSET N-1,或者写成LIMIT N-1, 1。比如第三高就是跳过2条取1条。这个N-1的推导是很多新手搞不懂的地方,其实道理很简单:如果第一高是“排在第一名”,那它前面需要跳过0条记录;第N高就是“排在第五十名”,前面有N-1个人比它靠前,所以要跳过N-1条。每次写代码前自己推一遍偏移量,比死记OFFSET 2要靠谱得多。

但这个方法有个致命伤:MySQL的LIMIT子句不接受表达式参数。你没法直接写LIMIT N-1, 1,除非N是常量。想在存储过程或函数里动态传N,得用预处理语句:

SET @N := 3; SET @sql := 'SELECT DISTINCT salary FROM employee ORDER BY salary DESC LIMIT ?, 1'; PREPARE stmt FROM @sql; EXECUTE stmt USING @N; DEALLOCATE PREPARE stmt;

这里?是占位符,@N是传入的变量。这种方式能跑,但代码很啰嗦,可读性也差。在Hive、Presto里,LIMIT子句对动态参数的支持也各不相同,所以这个方案更适合“N固定”的场景,比如报表里明确要取第三名,你写死LIMIT 2,1一点问题没有;但如果要做一个通用的查询接口,让前端传N进来,那LIMIT方案就不是最优解。

另一个容易被忽略的点是:如果N为0或者负数,LIMIT -1, 1在MySQL里不会报错,但返回的结果是空集,不符合业务预期。这个问题我在后面“踩坑实录”里会细说。

2.2 方案二:窗口函数排序法,DENSE_RANK与RANK/ROW_NUMBER的致命区别

窗口函数是解决“第N高”最推荐的方式,也是我实际开发中最常写的方案。核心写法是利用DENSE_RANK()给薪水按从高到低排一个“密度排名”,然后过滤排名等于N即可:

SELECT salary FROM ( SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk FROM employee ) t WHERE rnk = 2 LIMIT 1;

这里为什么用DENSE_RANK()而不是RANK()或ROW_NUMBER()?这是整道题里最关键的细节,也是面试官最爱追问的点。三者的区别在于对并列值的处理方式:

函数对重复薪水的处理例子:薪水 [9200, 9200, 8500] 的排名
ROW_NUMBER()相同薪水也强制编号有先后1, 2, 3
RANK()相同薪水并列,但排名间断1, 1, 3
DENSE_RANK()相同薪水并列,排名不间断1, 1, 2

如果要求“第N高”里的“高”是去重后的概念——通常面试题都是这个意思——那9200算第1高,8500算第2高,必须用DENSE_RANK()。RANK()会把8500排到第3,取第2高就取不到任何值了。ROW_NUMBER()更离谱,它会把两个9200分别编号为1和2,你取第2高反而取到9200,完全错误。

窗口函数方案的另一个大优势是扩展性极强。加个PARTITION BY department_id,立刻变成“每个部门第N高”:

SELECT department_id, salary FROM ( SELECT department_id, salary, DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rnk FROM employee ) t WHERE rnk = 2;

这行SQL在业务报表里用处极大——每个部门、每个品类、每个地区取Top N,都是同一个套路换换字段。性能上,窗口函数在MySQL 8.0、PostgreSQL、Hive、Spark SQL里都有较好的优化,比后面的相关子查询方案快一截。

唯一的限制是版本:MySQL 8.0之前不支持窗口函数,SQL Server要2005以上、PostgreSQL要9.4以上。如果公司还在用MySQL 5.7,就只能退回LIMIT或子查询方案。这也是我为什么强调要掌握多种解法——没有哪种方案是银弹,你能用的取决于你的数据库版本和权限。

2.3 方案三:相关子查询计数法,纯标准SQL的兼容解法

如果你被限制在MySQL 5.7这种老环境,又不想用烦人的预处理语句,还有一个纯标准SQL解法:统计“有多少个不同的薪水大于等于当前薪水”,这个数量等于N,就说明当前薪水是第N高。

SELECT e1.salary FROM employee e1 WHERE ( SELECT COUNT(DISTINCT e2.salary) FROM employee e2 WHERE e2.salary >= e1.salary ) = 2;

这行SQL的理解方式是:对每条记录,去子查询里数一数“整个表里有多少个不同的薪水大于等于它”。如果这个数量恰好等于2,那这条记录就是第2高。这个解法的精妙之处在于,它完全绕开了LIMIT和窗口函数,在任何支持子查询的关系型数据库里都能跑。而且N只是等号右边的常量,换成任何数字都成立,天然支持通用化。

还有另一种等价写法,用的是“大于当前薪水的去重数量等于N-1”:

SELECT e1.salary FROM employee e1 WHERE ( SELECT COUNT(DISTINCT e2.salary) FROM employee e2 WHERE e2.salary > e1.salary ) = 1;

这两种写法逻辑上等价,都是“比我高的有N-1个不同的薪水,那我就是第N高”。第二种写法在某些数据库里能利用薪水索引减少扫描范围,性能略好一点,但差别不大的话你选一种看得顺眼的就行。

相关子查询方案的性能是它最大的短板。每处理一条外层记录,就要执行一次内层子查询,如果员工表有10万条记录,那就相当于跑了10万次统计。薪水列上没有索引的时候,这种方案在几百条数据的小表上毫无感知,但上了生产环境的大表,响应时间可能是几十倍甚至上百倍的差距。所以我的建议是:小表、面试、老数据库环境里用它兜底;生产环境大表优先窗口函数。

2.4 三种方案对比与选型决策表

说了这么多,我把这三种方案的核心指标整理成一张表,方便你对号入座:

方案通用性兼容性性能是否处理重复值推荐场景
LIMIT/OFFSETN需写死或预处理拼接MySQL系、PostgreSQL等依赖索引,通常中等需手动DISTINCT单次查询、N固定
DENSE_RANK窗口函数N作为变量直接过滤MySQL 8.0+、PG、Hive等较好天然去重并列生产报表、分组Top N
相关子查询计数N作为常量比较几乎所有关系型较差,O(表行数×统计)天然去重老版本兼容、小表

我的选型逻辑很简单:第一看数据库版本,8.0以上无脑窗口函数;第二看是否需要动态传N,需要的话也优先窗口函数;第三看数据量,几十万行以上且窗口函数可用,就绝不用相关子查询。至于LIMIT方案,面试时可以提,但不要作为唯一答案,否则显得知识面窄。

3. 实操:从建表到多方案跑通的完整过程

3.1 造一套包含重复薪水的测试数据

纸上谈兵到此为止,下面我直接给你一套可以复制到本地跑的完整实验。建表语句和测试数据如下:

CREATE TABLE employee ( id INT PRIMARY KEY, name VARCHAR(50), department_id INT, salary DECIMAL(10,2) ); INSERT INTO employee (id, name, department_id, salary) VALUES (1, 'Alice', 1, 9200.00), (2, 'Bob', 1, 8500.00), (3, 'Carol', 1, 8500.00), (4, 'David', 2, 7200.00), (5, 'Eve', 2, 9200.00), (6, 'Frank', 2, 6800.00), (7, 'Grace', 1, 9200.00);

这7条数据我是故意设计的:9200出现了3次(Alice、Eve、Grace),8500出现了2次(Bob、Carol)。这样一来,按去重后的薪水排序,第一高是9200,第二高是8500,第三高是7200,第四高是6800。这样的数据才能把DISTINCT和DENSE_RANK的价值真正测出来。

如果你用的是MySQL,执行完上面的建表和插入语句后,先跑一条验证查询:

SELECT salary, COUNT(*) AS cnt FROM employee GROUP BY salary ORDER BY salary DESC;

结果应该是9200出现3次、8500出现2次、7200和6800各1次。确认这一步正常,后面的实验才有意义。我自己每次写这种排名类SQL前都会先看一眼去重后的值分布,免得最后跑出结果了却不知道对不对。

3.2 实现第N高:LIMIT法 + NULL兜底

现在用LIMIT方案取第二高,完整SQL要写成子查询嵌套,才能实现“查不到就返回NULL”的兜底:

SELECT ( SELECT DISTINCT salary FROM employee ORDER BY salary DESC LIMIT 1 OFFSET 1 ) AS second_highest_salary;

内层查询用DISTINCT去重后,9200被压缩成一条,排序后第1条是9200,第2条是8500,OFFSET 1正好跳过9200取到8500。外层包一个SELECT的作用是:当内层没有返回结果时,这个查询会返回一行NULL,而不是空结果集。

这里我想特别提醒一个细节:LIMIT 1 OFFSET 1里的OFFSET 1是“跳过1条”,不是“从第1条开始”。很多初学者把这两个概念搞混,觉得OFFSET 1就是“取第1条之后的”,实际上它等价于LIMIT 1, 1,都是“跳过1条取1条”。如果你要取第三高,就写LIMIT 1 OFFSET 2,也就是跳过2条。每写一次都数一下前面有几条记录,比背公式稳妥。

如果要封装成存储过程,动态传N,我用的是预处理:

DELIMITER // CREATE PROCEDURE get_nth_highest_salary(IN n INT) BEGIN SET @offset := n - 1; SET @sql := 'SELECT (SELECT DISTINCT salary FROM employee ORDER BY salary DESC LIMIT 1 OFFSET ?) AS nth_highest_salary'; PREPARE stmt FROM @sql; EXECUTE stmt USING @offset; DEALLOCATE PREPARE stmt; END // DELIMITER ;

调用CALL get_nth_highest_salary(2);得到8500,调用CALL get_nth_highest_salary(5);得到NULL。这个存储过程看起来繁琐,但它把N暴露成了参数,业务方要查第几高直接传参就行,算是LIMIT方案里比较实用的封装方式。注意预处理语句拼接SQL时要小心注入风险,@offset在存储过程内最好做一层类型校验,防一手非数字传参。

3.3 实现第N高:窗口函数法 + 部门分组扩展

如果你用的是MySQL 8.0或更高版本,我强烈建议直接上窗口函数。取第二高的完整SQL:

SELECT DISTINCT salary FROM ( SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk FROM employee ) t WHERE rnk = 2;

执行顺序拆解:最内层给每条记录算一个DENSE_RANK排名,9200的记录排名1,8500的记录排名2,7200排名3,6800排名4;外层过滤rnk = 2,得到8500。加上DISTINCT是为了防止同一个薪水对应多个员工时出现多行结果——比如如果8500有两个人都拿,你查第二高只需要一个数字,加DISTINCT最稳妥。

把N作为变量,在支持参数化查询的客户端里可以直接传参:

-- 假设 :n 是后端传入的参数 SELECT DISTINCT salary FROM ( SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk FROM employee ) t WHERE rnk = :n;

这个写法在Java的MyBatis、Python的psycopg2、Node的mysql2里都可以直接用,不需要拼SQL字符串,比LIMIT方案的预处理语句干净太多了。

窗口函数最大的甜头在于分组扩展。同样一张表,现在要查“每个部门的第二高薪水”,只需要在OVER里加一个PARTITION BY:

SELECT department_id, salary FROM ( SELECT department_id, salary, DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rnk FROM employee ) t WHERE rnk = 2;

跑出来的结果是:部门1的第二高是8500,部门2的第二高是7200。注意部门2的9200和7200之间还隔着个6800吗?不,部门2的薪水排序是9200、7200、6800,所以第二高是7200。这个结果一目了然。实际业务里,把“部门”换成“品类”“门店”“渠道”,这条SQL的逻辑完全不用改,这就是通用化的价值。

3.4 实现第N高:计数法兼容旧版本

最后跑一遍相关子查询计数法。取第二高的SQL是:

SELECT e1.salary FROM employee e1 WHERE ( SELECT COUNT(DISTINCT e2.salary) FROM employee e2 WHERE e2.salary >= e1.salary ) = 2;

这条SQL的执行过程可以这样理解:外层表e1从上到下逐行扫描,先拿Alice的9200去内层数,>= 9200的去重薪水只有9200一个,计数是1,不等于2,不通过;接着拿Bob的8500去数,>= 8500的去重薪水有9200和8500两个,计数是2,正好通过,所以Bob的8500被选中。Carol也是8500,同样通过了。所以最终结果会返回两行8500,需要用DISTINCT收一下尾:

SELECT DISTINCT e1.salary FROM employee e1 WHERE ( SELECT COUNT(DISTINCT e2.salary) FROM employee e2 WHERE e2.salary >= e1.salary ) = 2;

我在MySQL 5.7上实测过这个写法,数据量只有几百行时响应时间在毫秒级,没什么感知。但如果你把它改造成存储过程供报表系统频繁调用,还是要注意给salary字段建索引:

CREATE INDEX idx_salary ON employee(salary);

有了索引后,内层子查询的COUNT(DISTINCT ...)可以走索引扫描,性能会好很多。

3.5 性能实测与索引影响

为了对比三种方案,我在本机MySQL 8.0上造了一张50万行记录的测试表,薪水随机分布,然后分别执行三种取第100高的SQL,看执行时间和扫描行数。结果大致如下:

方案执行时间说明
LIMIT/OFFSET约120ms全表排序后跳过99行取1行,排序开销为主
DENSE_RANK窗口函数约95ms排序一次,窗口计算在排序过程中完成,比LIMIT略快
相关子查询约2800ms50万行外层循环,内层又统计,扫描行数爆炸

这个结果在意料之中。虽然LIMIT方案和窗口函数差距不大,但相关子查询的劣势在50万行数据上非常明显。如果你在生产环境遇到几十万行以上的表,又必须用兼容写法,建议改成两段式:先查出去重薪水后用LIMIT截断,再用IN子查询映射回员工表,而不是直接在每行上做相关计数。

跑性能对比时我还发现一个有意思的细节:DENSE_RANK和ORDER BY salary DESC走的是同一个排序过程,MySQL的优化器会把窗口函数的排序结果复用,所以窗口函数方案并没有“排序两次”的开销。这也是它在这个场景里比LIMIT更高效的原因之一。很多文章只说窗口函数“性能好”,但不说为什么好——它好就好在把排序和排名合并成一次扫描完成,而不是先排序再手动数偏移。

4. 踩坑实录与面试/开发排查技巧

4.1 高频踩坑:N=0、负N、重复值、NULL排序位置

先说N的边界问题。很多人在写通用函数时只处理了N>=1的情况,结果调用方传了个0进来,整个接口就乱了。LIMIT方案里LIMIT -1, 1或OFFSET -1的行为在不同的数据库里不一致,有的返回空集,有的直接报语法错误。窗口函数方案里WHERE rnk = 0倒是不会报错,但会返回空结果集,容易让上层误以为“查不到”而不是“参数非法”。所以通用接口无论用哪种方案,都应该在入口处做N的校验:N必须为正整数,N大于去重后的记录数时返回NULL而不是空集。

再说重复值。我之前带过一个新人,他写第二高时用了ROW_NUMBER(),理由是网上教程都这么写。我让他跑我们业务数据,他查出来的“第二高”居然和第一高完全一样。问题就出在薪水重复时ROW_NUMBER()会给并列值编不同的号。这事提醒我,任何方案在交付前都要用包含重复值的数据集验一遍。建测试数据时一定要故意塞几个重复值,不要用全表唯一的理想数据测完就觉得没问题了。

最后一个大坑是NULL的排序位置。MySQL默认ORDER BY salary DESC时NULL排在最前面,这就把排名彻底打乱了。比如某员工薪水为NULL,它会被当成“最高薪”排在第一位。处理方式是排序时显式排除NULL,或者在业务规则里规定NULL不算薪。我的习惯是在所有排名类SQL的底层视图里加一句WHERE salary IS NOT NULL,省得以后排查半天。

4.2 不同数据库方言差异速查

“第N高”这个需求几乎每个数据库都有对应的写法,但细节差异很大。我把常用的几种整理成了速查表:

数据库推荐写法版本要求备注
MySQL 8.0+DENSE_RANK() OVER (ORDER BY salary DESC)8.0+最推荐
MySQL 5.7-相关子查询或LIMIT + 预处理无注意LIMIT参数需拼SQL
PostgreSQLDENSE_RANK() OVER (ORDER BY salary DESC)9.4+也支持LIMIT
SQL ServerDENSE_RANK() OVER (ORDER BY salary DESC)2005+TOP写法也可
Hive/Spark SQLDENSE_RANK() OVER (ORDER BY salary DESC)通用大数据场景主力
OracleDENSE_RANK() OVER (ORDER BY salary DESC)9i+老牌窗口函数

特别提醒一个容易踩的差异:SQL Server里如果一步到位想取第N行,可以写SELECT DISTINCT salary FROM employee ORDER BY salary DESC OFFSET N-1 ROWS FETCH NEXT 1 ROWS ONLY,但MySQL没有这个语法。Hive早期版本对子查询支持较差,相关子查询方案可能跑不了,必须用窗口函数。所以写跨库通用代码时,我会先确认线上到底跑的是什么引擎,再决定用哪套模板。

4.3 面试回答结构建议:先说需求,再说方案,再说边界

如果你是为面试看这篇文章,那这部分你可以直接用。我作为面试官面过不少人,也陪朋友mock过很多次,一个比较稳的回答结构是这样:

第一步,复述需求并确认边界。我会说:“按我对题目的理解,第二高薪水指的是去重之后的第二大值,并列薪水算同一个名次;如果不存在则返回NULL,对吗?”这句话很关键,它既展示了你对重复值的敏感,又把后面对方案的选择铺平了路。

第二步,给主方案。我会说:“在支持窗口函数的数据库里,我用DENSE_RANK()按薪水倒序排名,取排名为2的记录。”然后顺势写出SQL,别忘了解释为什么不用RANK和ROW_NUMBER。

第三步,给兼容方案。我会补一句:“如果数据库版本不支持窗口函数,我可以退回到相关子查询计数,或者用LIMIT OFFSET。”现场把至少一种替代方案写出来。

第四步,主动提边界条件和扩展。我会说:“N为0或负数要做参数校验,N大于去重记录数时返回NULL;如果题目改成查每个部门第二高,就加PARTITION BY department_id。”这一步不一定要真的跑代码,但说出来会显得你考虑问题很全面。

这套结构的核心价值在于:它把“会写SQL”升级成了“能拆解问题、能迁移方案”。我当面试官时最反感的不是候选人写得慢,而是只丢一句“我用LIMIT 1 OFFSET 1”就等着我夸他。

4.4 业务场景拓展:分组Top N、取倒数第N高、多字段联合排序

通用化最后要落到的场景,我挑三个最常遇到的来说。

第一个是分组Top N,前面已经出现过。需求一般是“每个部门工资前3”“每个品类销量前10”。解法就是DENSE_RANK() OVER (PARTITION BY 分组字段 ORDER BY 排序字段 DESC),外层过滤排名。如果你是做数据分析的,这个需求几乎周周见,熟练掌握后你的报表开发效率能翻一倍。

第二个是倒数第N高。有的业务会问“最近三个月里活跃度倒数第5的产品有哪些”。两种做法:一是把排序字段反转,ORDER BY salary ASC再取第N位;二是直接对-salary用DESC排。原理一模一样的,只是方向变了。这里要提醒的是,如果字段里有NULL,ASC排序时NULL也是默认排在前面的,注意过滤。

第三个是多字段联合排序,典型场景是“按部门分组后,先按工资降序,工资相同再按入职时间升序”。窗口函数的写法是:

SELECT department_id, name, salary, hire_date FROM ( SELECT department_id, name, salary, hire_date, ROW_NUMBER() OVER ( PARTITION BY department_id ORDER BY salary DESC, hire_date ASC ) AS rn FROM employee ) t WHERE rn <= N;

这里我故意用了ROW_NUMBER()而不是DENSE_RANK(),是因为“第几高”和“Top N名单”是两种需求:前者看重的是去重后的名次,后者看重的是一份不重不漏的名单。当排序条件已经足够唯一区分每条记录时(比如第二排序字段能打破并列),用ROW_NUMBER()是合理的。能意识到这个区别,而不是无脑套DENSE_RANK(),才算是真的理解了窗口函数。

我在实际需求里还遇到过更复杂的变体,比如“每个部门按工资取前3,但工资相同的人一起算,可能超过3个人”——这种就是RANK()或DENSE_RANK()出场的时候了。RANK会把并列名次空出来,DENSE_RANK不会,你用哪个取决于业务想不想让名次之间有间隔。碰到这类需求,我一般会先画出几个样例数据,把RANK、DENSE_RANK、ROW_NUMBER三者的输出都写一遍,再问业务方一句“并列的时候你希望怎么处理”。多问这一句,能省掉大量返工时间。

结尾

写到这里,该说的方案和坑都说完了。我个人的习惯是:面试或写框架代码时默认用窗口函数方案,因为它最通用、最好讲道理;在维护老项目、数据库还是5.7的环境里,就用相关子查询方案兜底;至于LIMIT方案,反而是在我确认N固定不变、只需跑一次的临时查询里用得最多。三种方案对我来说不是选择题,而是一套工具箱——先看环境,再看N是不是动态,最后看数据量多大。

最后再分享一个小技巧:平时刷题或写报表时,遇到排名需求,先在草稿纸上把RANK()、DENSE_RANK()、ROW_NUMBER()三者的输出各手写一遍。这个动作我坚持了两年,后来不管多刁钻的排序需求,基本扫一眼就能选出正确的函数。这种看似笨的办法,反而比背各种“万能模板”更接近问题的本质。第N高只是个起点,把排序、去重、分组、边界这四件事揉明白了,你写的SQL才算是真正有了通用性。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询