拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

SQL刷题进阶指南:窗口函数、自连接与性能优化的核心要点

SQL刷题进阶指南:窗口函数、自连接与性能优化的核心要点

最近抽空把LeetCode上的SQL题重新刷了一遍,趁着知识点还热乎,把整个练习过程中的思路、踩坑和总结整理成这份记录。说实话,很多人刷LeetCode只盯着算法题,对SQL板块不太当回事,觉得"SQL嘛,写几个SELECT就完事了"。但实际刷下来你会发现,这里的SQL题虽然叫easy、medium,真正动手写的时候,坑比想象中多得多——尤其是那些业务里天天写SQL的人,反而最容易在看似简单的题上翻车。

这份记录主要写给三类人:准备数据开发或后端岗位面试的人、日常写业务SQL但想系统补一下薄弱环节的人、以及刚入门SQL想找点练习题巩固基础的初学者。我不会只贴题目答案,而是把每类题型的思考路径、判断依据和容易出错的细节讲清楚,配合实际可跑的SQL语句,照着练就能看到效果。

1. 为什么刷SQL题这件事值得认真对待

1.1 刷题和写业务SQL,中间隔着一条认知鸿沟

日常工作中我们写的SQL大多是业务查询,比如查订单、统计用户数、关联几张表出报表。这种SQL写多了,人会形成一种"够用就行"的惯性:能用子查询就用子查询,能多写几步就多写几步,反正数据量不大、跑得也不慢,结果对就行。

但LeetCode的SQL题不一样。它逼着你在一个非常受限的题目描述里,用尽量简洁、正确的SQL去解决问题。题目不会告诉你"你可以先建个临时表再查",也不会说"用程序代码处理一下也行"。你只能靠纯SQL把逻辑表达出来。这恰好暴露了一个问题:很多写了好几年SQL的人,对JOIN、GROUP BY、窗口函数这些核心语法其实是"半懂不懂"的状态。会用,但不知道为什么这么用;能跑出结果,但边界条件一加就挂。

我刷题过程中最大的感受就是:业务SQL的"能用"和刷题SQL的"正确"是两个层次的东西。业务SQL只要结果对,过程丑一点无所谓的;但刷题SQL要求你完全掌控每一步的语义,包括NULL怎么处理、去重按什么标准去、排序的并列情况怎么算,这些在业务里经常被忽略的细节,恰恰是面试官最喜欢挖的地方。

1.2 不同基础的人,刷题的目标不一样

如果你是SQL新手,刷LeetCode SQL题的主要价值在于强制你把手动查询的逻辑翻译成SQL语法。你不需要纠结题目难不难,先按自己的理解写,写错了再看讨论区,这个过程本身就是语法和思维的快速磨合。

如果你是有经验的开发或数据分析师,刷题的重点就不是语法了,而是查漏补缺和思维升级。比如我这次刷题最大的收获之一,就是彻底把窗口函数的应用场景给捋顺了。以前在业务里遇到"分组后取每组Top N"这种需求,我总想着用自连接或者临时表去搞,窗口函数一行就解决了。这种"原来还能这么写"的顿悟,只有通过集中的题目训练才能密集出现。

刷题是为了什么?不是去背题,而是通过题目这个载体,把SQL的各个语法模块在脑子里形成一个有结构的认知地图。下次遇到需求的时候,你能在第一时间想到对应的解法,而不是在百度里搜"SQL 分组取最大值怎么实现"。

2. 高频题型拆解:关联、聚合、排名三类问题的通用套路

2.1 多表关联JOIN条件的边界感

LeetCode的SQL题里,多表关联是最基础也是最常考的一类。题目描述通常给两张或三张表,让你查"有订单的用户""没买过东西的客户""工资比经理高的员工"等等。

这种题的核心就一句话:搞清楚两表之间的关联键和保留方向。INNER JOIN保留两边的匹配记录,LEFT/RIGHT JOIN保留一侧的全部记录,FULL JOIN保留两边的全部记录。LeetCode里考得最多的是LEFT JOIN,因为它天然适合"查A表里有但B表里没有"的场景。

举一个经典的例子,"从不订购的客户"(LeetCode 183):

SELECT c.name AS Customers FROM Customers c LEFT JOIN Orders o ON c.id = o.customerId WHERE o.customerId IS NULL;

这题的关键不在LEFT JOIN本身,而在WHERE条件必须过滤右表主键为空。我见过不少人写成:

SELECT c.name AS Customers FROM Customers c LEFT JOIN Orders o ON c.id = o.customerId WHERE o.id IS NULL OR o.customerId IS NULL;

两个条件的效果其实一样,因为只要右表有一列为空,整条关联记录基本就是无匹配的。但更常见的错误是写成WHERE o.customerId = NULL,这是SQL初学者必踩的坑——NULL不能跟任何值用等号比较,必须用IS NULL。

还有一个边界情况值得注意:JOIN条件里的字段如果本身允许NULL,那LEFT JOIN之后过滤就要格外小心。比如关联键是a.id = b.user_id,如果b.user_id存在NULL值,那么这些NULL值在JOIN时不会匹配到任何左表记录,但LEFT JOIN后它们会保留在左表侧,此时用WHERE b.some_field IS NULL做过滤,可能会把原本有匹配但匹配字段恰好为NULL的记录也筛掉。所以刷多了你会发现,设计表结构时尽量避免关联键为NULL,这是很多线上系统会额外加非空约束的原因。

2.2 聚合分组WHERE和HAVING的分工

聚合类的题目在LeetCode里出现频率也很高。核心考点就两个:GROUP BY的分组粒度,以及WHERE和HAVING到底哪个先执行。

先明确执行顺序:WHERE是对原始行做过滤,过滤完之后才进行GROUP BY分组和聚合计算;HAVING是对分组后的结果做过滤。所以如果一个条件针对的是某一行,放在WHERE里;针对的是聚合结果(比如COUNT(*) > 1、SUM(amount) > 100),放在HAVING里。

看一个典型的例子,"至少有五名直接下属的经理"(LeetCode 570):

SELECT m.name FROM Employee e JOIN Employee m ON e.managerId = m.id GROUP BY m.id, m.name HAVING COUNT(e.id) >= 5;

这里用自连接把员工和经理关联起来,按经理分组,然后用HAVING过滤出直接下属数量大于等于5的经理。如果把HAVING换成WHERE,SQL直接报错——因为聚合函数不能出现在WHERE子句里。这是很硬性的语法规则,初学者经常在这里卡住。

聚合的另一个容易坑人的点,是GROUP BY后面的字段和SELECT后面非聚合字段的对应关系。MySQL有一个比较宽松的模式,允许SELECT里出现没有被GROUP BY的字段,但结果是不确定的;其他数据库(比如SQL Server、PostgreSQL)会直接报错。建议练习时严格遵守"SELECT里出现的非聚合字段,必须全部出现在GROUP BY中"这个规范,养成好习惯。

2.3 排名与去重:ROW_NUMBER、DISTINCT、GROUP BY怎么选

排名和去重是SQL题里的"高频钉子户"。"第N高的薪水"(LeetCode 176)、"分数排名"(LeetCode 178)、"部门工资前三高的员工"(LeetCode 185)这类型的题目考的就是对去重和排名函数的理解深度。

先说去重。SQL里去重有三种思路:DISTINCT、GROUP BY、ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)。很多人只知道DISTINCT,但遇到"按某个字段去重,取另一个字段最大的那条记录"这种需求时,DISTINCT就无能为力了。

举个例子,"每个部门工资最高的员工"(LeetCode 184)。最直观的思路是先用GROUP BY departmentId找到每个部门的MAX(salary),再关联回原表:

SELECT d.name AS Department, e.name AS Employee, e.salary AS Salary FROM Employee e JOIN Department d ON e.departmentId = d.id WHERE (e.departmentId, e.salary) IN ( SELECT departmentId, MAX(salary) FROM Employee GROUP BY departmentId );

这里用了一个多字段IN的写法,等价于"匹配部门ID和最高薪水的组合"。这种写法不算最优,但非常直观,而且不容易出逻辑错误。还有一种写法是自连接,把两张Employee表按部门关联,再通过HAVING salary = MAX(salary)来过滤。两种思路都可行,重点是你得能说出为什么需要两步而不是一步——因为"先求最大值"和"再找对应员工"是两个独立的逻辑步骤。

至于排名,LeetCode里同时会用到三个函数:ROW_NUMBER()、RANK()、DENSE_RANK()。三者的区别必须烂熟于心:

函数并列处理排名是否跳号典型场景
ROW_NUMBER()相同分数随机/按附加顺序编号不并列,编号连续取分页、取第N条
RANK()并列占用同一名次会跳号(如1,1,3)体育竞赛排名
DENSE_RANK()并列占用同一名次不跳号(如1,1,2)部门工资前三高

LeetCode 185 要求"部门工资前三高的所有员工",因为涉及并列,要用DENSE_RANK():

SELECT Department, Employee, Salary FROM ( SELECT d.name AS Department, e.name AS Employee, e.salary AS Salary, DENSE_RANK() OVER (PARTITION BY e.departmentId ORDER BY e.salary DESC) AS rk FROM Employee e JOIN Department d ON e.departmentId = d.id ) t WHERE rk <= 3;

子查询里算排名,外层过滤rk <= 3。我以前刚开始刷题时总想着把排名条件塞到WHERE里,直接报错——窗口函数在WHERE之后执行,所以必须包一层子查询。这个知识点几乎每次遇到窗口函数题都会用到,属于必背结论。

3. 刷题过程中最容易被扣分的几个思维死角

3.1 SELECT各子句的实际执行顺序

很多教程讲SQL都是按SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY这个顺序教的。但实际执行顺序完全不同,如果脑子里没有这个执行顺序的模型,很多题你会觉得"我逻辑没问题啊,为什么结果不对"。

标准SQL的逻辑执行顺序是:

  1. FROM:确定数据源,执行JOIN
  2. WHERE:按条件过滤原始行
  3. GROUP BY:按指定列分组
  4. HAVING:过滤分组结果
  5. SELECT:计算目标列、别名、聚合
  6. ORDER BY:排序
  7. LIMIT / OFFSET:分页

我举一个非常典型的例子,"上升的温度"(LeetCode 197)。表结构很简单:Weather(id, recordDate, temperature),要求查出所有比前一天温度高的记录日期ID。常规思路是利用自连接,把今天的记录和昨天的记录拼在一起:

SELECT w1.id FROM Weather w1 JOIN Weather w2 ON DATEDIFF(w1.recordDate, w2.recordDate) = 1 WHERE w1.temperature > w2.temperature;

JOIN条件用了DATEDIFF = 1表示"今天 = 昨天+1天"。这里如果不懂执行顺序,可能会纠结 "WHERE 应该先过滤日期还是先比较温度"——其实不影响,JOIN已经把符合条件的记录拼好了,WHERE只是做最终过滤。

但真正容易踩坑的是如果表里同一天有多条记录,或者日期跨度不连续,DATEDIFF = 1这种硬性差一天的条件就会失效。更稳妥的写法是用窗口函数LAG取前一天温度:

SELECT id FROM ( SELECT id, temperature, LAG(temperature) OVER (ORDER BY recordDate) AS prev_temp, LAG(recordDate) OVER (ORDER BY recordDate) AS prev_date FROM Weather ) t WHERE temperature > prev_temp AND DATEDIFF(recordDate, prev_date) = 1;

窗口函数先按日期排序,再取上一行的温度和日期,最后外层过滤。这种写法比自连接更接近业务直觉,也更不容易漏数据。但前提是同一个日期只有一条记录,如果同一天有多条,两个写法都会出问题,需要额外按日期去重——这就是刷题复盘的价值,你能提前意识到这些边界问题。

3.2 NULL不是一个值,它是一个状态

SQL 里 NULL 可能是最反直觉的东西了。它不是一个具体的值,而是"未知"或"缺失"的标记。因为它是未知的,所以NULL = NULL的结果不是TRUE,而是NULL(即不是真也不是假,是未知)。这导致了很多经典错误。

最常见的错误是WHERE column = NULL,这个条件永远不会匹配任何行。正确写法永远是IS NULL或IS NOT NULL。这个坑在"从不订购的客户"这类LEFT JOIN题里几乎必踩。

还有一个更隐蔽的坑:COUNT(column)会忽略NULL值,而COUNT(*)不会。如果某字段很多NULL,COUNT(column)的结果会比实际行数小。面试经常拿这个来盘你:"一张表有100行,某字段有20个NULL,COUNT(*)、COUNT(字段)、COUNT(DISTINCT 字段)分别返回多少?"答案是100、80、以及去重后的非NULL值数量。刷题如果遇到涉及统计的题,要多想一层:题目到底要数"所有行"还是"非NULL的行"。

此外,NULL在排序中也很有讲究。默认升序时,SQL Server 和 MySQL 把 NULL 排在最前面,PostgreSQL 和 Oracle 把 NULL 排在最后面。LeetCode 的判题系统在不同数据库下结果可能不同,所以遇到ORDER BY排序题时,建议显式处理 NULL 的位置,比如ORDER BY column ASC NULLS LAST,确保逻辑在哪个数据库下都是确定性的。

3.3 自连接的典型用法与笛卡尔积事故现场

自连接指的是表和自己做JOIN。LeetCode 里考"经理工资比员工高""连续出现的数字""游戏玩法分析"这类题时,自连接几乎是标配。但自连接的代价是容易产生笛卡尔积,一旦JOIN条件写松了,数据量会爆炸性增长。

拿"连续出现的数字"(LeetCode 180)来说,题目要求找出所有至少连续出现三次的数字。思路之一就是三次自连接,用l1.id = l2.id - 1 AND l2.id = l3.id - 1来限定相邻记录:

SELECT DISTINCT l1.num AS ConsecutiveNums FROM Logs l1 JOIN Logs l2 ON l1.id = l2.id - 1 JOIN Logs l3 ON l2.id = l3.id - 1 WHERE l1.num = l2.num AND l2.num = l3.num;

JOIN条件必须精确到id相邻,否则一张大表自连接三次,中间结果集可能是原始行数的三次方量级,线上这么写会直接把数据库拖垮。我当时第一次写这个题,没加id的递进条件,只写了WHERE l1.num = l2.num AND l2.num = l3.num,结果跑出来一堆重复记录不说,执行时间直接拉满。这是因为没限定JOIN的行间关系,所有相同数字的行都互相匹配了。

自连接的正确姿势是先把连接条件限定到最小范围(相邻、同组、日期差1等),再用WHERE过滤业务条件。连接的目的是把横向的记录变成一行里可比较的字段,条件写精确了,结果才可控。

4. 窗口函数:从“看得懂”到“写得出”的跨越

4.1 窗口函数解决的经典问题:分组TopN与累计计算

如果你在网上搜SQL优化的文章,经常看到别人推荐用窗口函数替代子查询和自连接。窗口函数确实强,但它最擅长的其实是两类问题:分组TopN和累计/移动计算。

分组TopN的代表作就是"部门工资前三高的所有员工"(LeetCode 185),上一节已经写过。窗口函数在这一类问题上的语法非常统一:

ROW_NUMBER() / RANK() / DENSE_RANK() OVER ( PARTITION BY 分组字段 ORDER BY 排序字段 )

PARTITION BY负责分组,ORDER BY负责组内排序,函数整体负责生成组内序号。只要记住这个固定句式,绝大多数TopN问题都能套。

累计计算的代表作比如"游戏玩法分析"中的"每位玩家第一次登录平台的日期"(LeetCode 511),虽然这题用聚合更简单,但更典型的累计场景是"每日新增用户累计数"。这类题用窗口函数的SUM() OVER (ORDER BY 日期)就能逐行累加:

SELECT date, daily_new_users, SUM(daily_new_users) OVER (ORDER BY date) AS total_users FROM daily_stats;

这里的逻辑就是:SUM配合ORDER BY,生成一个从第一行累加到当前行的累计值。如果加上PARTITION BY,还能做到"分组内累计"。日常做报表统计时,这种写法比"每条记录再连一张子查询求小于当前日期的总和"要高效得多,一条SQL搞定,可读性也好。

4.2 PARTITION BY和ORDER BY的配合技巧

窗口函数里最容易搞混的是PARTITION BY和ORDER BY的组合效果。一句话总结:PARTITION BY决定窗口怎么划分,ORDER BY决定每个窗口内部怎么排序以及窗口从哪一行开始算到哪一行结束。

没有ORDER BY时,整个PARTITION是一个整体,窗口函数的值在组内是同一个(比如全组求和、全组最大)。有了ORDER BY后,默认窗口是从分区第一行到当前行,相当于一个"累计"范围。这就是为什么SUM(amount) OVER (PARTITION BY user_id ORDER BY date)会得到"每个用户按日期累计的消费额",而不是"每个用户的总消费额"。

如果你想在窗口里明确指定范围,可以用ROWS BETWEEN子句。比如计算最近3天的移动平均:

SELECT date, amount, AVG(amount) OVER (ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS avg_3d FROM sales;

ROWS BETWEEN 2 PRECEDING AND CURRENT ROW的意思是"当前行往前推两行到当前行",形成一个三行的滑动窗口。这种语法在LeetCode的medium难度题里偶尔出现,业务里做数据分析和指标计算时会很常用。我建议在刷题时遇到一次就专门练透,因为这种写法用熟了之后,回来看自连接版本简直觉得是在绕远路。

窗口函数还有一个很容易被忽略的细节:在同一个查询里可以定义多个窗口,并且窗口之间可以独立指定。比如既想按地区分组算排名,又想按地区分组累加销量,就可以写两个OVER子句,互不影响。这在业务里做"排行榜 + 累计趋势"二合一报表时非常实用。

5. 从LeetCode走向实战:SQL练习之外的自我检视

5.1 慢SQL优化思路:从执行计划到索引

刷题刷到一定量之后,你会发现LeetCode上的SQL题多数只关心"逻辑正确",对性能要求不高。但回到实际生产环境,SQL写得再对,跑不动一样白搭。所以刷题之外,我还建议你专门花时间补一下慢SQL优化这件事。

慢SQL优化的起点是先看执行计划,不要凭空猜。MySQL 用EXPLAIN,SQL Server 用"显示估计的执行计划",PostgreSQL 用EXPLAIN ANALYZE。执行计划里最核心的几项是:有没有走索引(type字段是不是const/ref/range而不是ALL)、扫描的行数(rows)、有没有Using filesort或Using temporary。

举一个最常见的优化场景:一张订单表有几百万行,你按用户的注册时间过滤,再按订单时间排序取最近10条。如果两个过滤条件都有单独的索引,MySQL会选其中一个,然后对结果做排序;如果数据量一大、排序字段没索引,就会出现Using filesort,性能直接掉一个数量级。优化方案通常是建复合索引,把过滤字段和排序字段放进同一个索引,例如(user_id, order_time),然后只按user_id过滤、按order_time排序。这样索引天然有序,排序步骤就省掉了。

LeetCode里虽然不太考执行计划,但你把刷题时常用的JOIN、GROUP BY、窗口函数拿到EXPLAIN下跑一遍,能直观看到不同写法在数据量放大后的差距。比如前面的自连接三连,如果id字段有主键索引,执行计划里是三个index lookup;如果没索引,直接就是三个全表扫描嵌套,灾难级别。所以我一直觉得,刷题是练逻辑,EXPLAIN是练手感,两个都别落下。

5.2 数据库方言差异:同样一句SQL在不同库里的表现

LeetCode 的判题主力是MySQL,但实际工作里你可能会用到SQL Server、Oracle、PostgreSQL、Hive等。热词榜里SQL Server相关的搜索量一直不低,说明很多人在本地练习时会选SQL Server作为环境。这里就有个很常见的坑:MySQL能跑的SQL,换到SQL Server未必能跑通。

我举几个典型的方言差异:

  • 字符串拼接:MySQL 用CONCAT()或双竖线(||需要开PIPES_AS_CONCAT模式),SQL Server 用+,Oracle 和 PostgreSQL 用||。
  • 分页:MySQL 用LIMIT offset, count,SQL Server 用OFFSET ... FETCH或TOP,Oracle 老版本用ROWNUM。
  • 取当前日期:MySQL 是CURDATE(),SQL Server 是GETDATE(),PostgreSQL 是CURRENT_DATE。
  • 字符串截取:MySQL 是SUBSTRING(str, pos, len),SQL Server 也是SUBSTRING,但Oracle 用SUBSTR,且下标从1开始(大家都从1开始,但某些老库有0的情况)。
  • 空值排序:前面提过,MySQL默认NULL排最前,PostgreSQL默认NULL排最后。
  • 自增主键:MySQL 是AUTO_INCREMENT,SQL Server 是IDENTITY(1,1),PostgreSQL 是SERIAL或IDENTITY。

刷题时如果你只用MySQL在线评测,有些题目偷懒用MySQL的方言写法也能过,但建议还是尽量写标准SQL。标准SQL在绝大多数数据库里都能直接跑或改个函数名就能跑,迁移成本低。我个人的习惯是:MySQL和PostgreSQL本地各装一套,遇到拿不准的写法两个库都验证一遍,这种习惯在实际工作中写跨库同步脚本时帮了我大忙。

5.3 写出可维护SQL的职业习惯

最后聊一点刷题之外、但对实际工作很有用的东西——SQL代码的可维护性。

我在SQL Server环境下排查慢SQL时,经常看到一长串动辄几百行的SQL,没有格式化、没有注释、子查询嵌套七八层。这种SQL逻辑再正确,后续接手的人根本不敢动。LeetCode的题虽然简单,但它给了你一个很好的机会去养成好习惯:

第一,缩进和关键字大写保持一致。SELECT、FROM、WHERE、JOIN这些关键字统一大写,字段和表名小写,一眼就能分清语法骨架和业务字段。

第二,复杂逻辑用通用表表达式(CTE)拆开。不要把所有逻辑堆在一个子查询里。比如先算paid_users,再算active_users,最后JOIN。每一层CTE只干一件事,命名清楚,别人读你的SQL就像读文章段落。

第三,JOIN条件写在ON里,过滤条件写在WHERE里。虽然把过滤条件写到ON里对于INNER JOIN来说结果一样,但LEFT JOIN的时候区别很大,写在WHERE里会变成"先关联再过滤",导致左表保留的记录减少,语义完全变了。规范统一,能避免很多低级事故。

第三点值得展开说。比如你要查"所有用户及其订单金额",有些订单可能已作废要排除。如果写成:

SELECT u.id, o.amount FROM Users u LEFT JOIN Orders o ON u.id = o.user_id AND o.status = 'valid';

这个查询会保留所有用户,无效订单显示为NULL;但如果写成:

SELECT u.id, o.amount FROM Users u LEFT JOIN Orders o ON u.id = o.user_id WHERE o.status = 'valid';

同样只有INNER JOIN的效果,没有有效订单的用户直接不出现了。语义完全不同。刷题时多练习这种"ON和WHERE分职责"的写法,实际工作中能少返工很多次。

6. 一些刷题过程中的个人习惯与工具推荐

最后顺手分享几个我刷题时积累的小工具和习惯,不算什么高级技巧,但对效率提升挺明显。

本地环境选型。LeetCode的SQL题在线编译器很方便,但有个问题,它只能验证结果,看不到执行计划,也不能自由造数据。我刷到中后期,凡是遇到窗口函数、性能对比这种需要反复试验的题,就直接在本地开SQL Server或MySQL跑。本地库有个好处是可以自己造边界数据,比如插入重复记录、NULL记录、同一天多条记录,看看自己的SQL在"脏数据"下还能不能出正确结果。这个习惯让我提前发现了很多题解的隐性假设。

维护一个错题文档。我刷题不是每题都过,凡是第一次没写对、或者看了题解才恍然大悟的题,都会单独记录在一个Markdown文档里,内容包括:题目编号、我当时的错误写法、错误原因分析(逻辑错误还是SQL特性不了解)、正确解法、以及本题涉及的知识点标签。比如"176题——考点:NULL处理、LIMIT、"这行记录,后续复习时扫一眼就能串起知识脉络。

按知识点刷,不按难度刷。LeetCode的SQL题是按easy、medium、hard分级的,但同一个知识点可能分散在多个难度里。我建议第一遍按标签分类刷,比如把JOIN相关的题都找出来集中做,做完对比它们之间的异同,比一天刷十道不同知识点的题效果好得多。等知识点都过了一遍,第二遍再按难度刷,相当于一次综合考试。

多写几个版本,再对比优劣。同一道题,子查询能写,自连接能写,窗口函数也能写。我刷题时要求自己至少写出两种解法,然后分析它们的可读性、性能和适用边界。比如"每个部门工资最高的员工",我用过IN嵌套子查询、自连接 +MAX、窗口函数三种写法。实际面试的时候,面试官不一定要求你写最优解,但如果你能主动说"另一种写法更利于大表查询",这会是明显的加分项。

SQL的学习路径其实很线性:语法基础打牢,然后刷题练逻辑,再回到生产环境练性能。LeetCode只是这条路径的中间一环,既不神秘也不高深,但对把SQL从"会写"推向"写得好"确实是一条捷径。希望这份练习记录能给你提供一点参考,也欢迎你拿着题来和我讨论更好的解法。

返回列表