ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SQL第N高薪水查询:从LIMIT到窗口函数的通用解法

SQL第N高薪水查询:从LIMIT到窗口函数的通用解法 提到“第二高薪水”只要是写过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, 3RANK()相同薪水并列但排名间断1, 1, 3DENSE_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、Grace8500出现了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条是8500OFFSET 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或更高版本我强烈建议直接上窗口函数。取第二高的完整SQLSELECT DISTINCT salary FROM ( SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rnk FROM employee ) t WHERE rnk 2;执行顺序拆解最内层给每条记录算一个DENSE_RANK排名9200的记录排名18500的记录排名27200排名36800排名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 BYSELECT 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 高频踩坑N0、负N、重复值、NULL排序位置先说N的边界问题。很多人在写通用函数时只处理了N1的情况结果调用方传了个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.0DENSE_RANK() OVER (ORDER BY salary DESC)8.0最推荐MySQL 5.7-相关子查询或LIMIT 预处理无注意LIMIT参数需拼SQLPostgreSQLDENSE_RANK() OVER (ORDER BY salary DESC)9.4也支持LIMITSQL ServerDENSE_RANK() OVER (ORDER BY salary DESC)2005TOP写法也可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排。原理一模一样的只是方向变了。这里要提醒的是如果字段里有NULLASC排序时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才算是真正有了通用性。
返回列表