从LeetCode数据库题库到实战:SQL刷题方法论与核心知识点总结
2026/9/18 14:40:43 网站建设 项目流程

我最早刷LeetCode的时候,压根没想到数据库题会这么有用。当时只是觉得算法题刷累了,换个口味做两道SQL题,结果一发不可收拾。数据库在LeetCode上是一个独立分类,叫做“Database”,题目数量虽然没有算法题那么夸张,但覆盖的知识面非常广,从最简单的单表查询、增删改查,到JOIN、子查询、聚合函数、窗口函数,再到连续性问题、排名问题、会员活跃度分析这类业务味道很浓的题,几乎把一条SQL从业者日常能用到的主干技能都串起来了。不管是准备面试、应付数据库课程设计,还是单纯想把MySQL、Oracle这类关系型数据库用得更熟,这个题库都值得仔细啃。这篇就把我从一个数据库小白到把这些题刷穿的过程、踩过的坑、总结出来的一套刷题方法论,完整写下来。

1. 数据库题库到底在考什么:整体认识与分段策略

1.1 难度分层与真实考点

先聊一个很多新手容易误解的事情:LeetCode数据库题并不是简单地把算法题换了个马甲。它的难度梯度设计得很有意思。

简单题主要集中在单表操作,考察的是你对基础SQL语法的掌握程度。比如查询某个条件下有多少条记录、按某个字段排序取前几条、对某个字段去重等等。这些题通常只需要一张表,在你理解需求后,五分钟内能写出答案,算是热手题。但千万不要小看这些简单题,它们踩坑的点不少,比如NULL值的处理、去重该用DISTINCT还是GROUP BY、字符串比较时隐式类型转换的问题,这些细节往往就是面试官喜欢追问的地方。

中等题开始出现多表关联、聚合统计、子查询这类进阶操作。你需要同时考虑多张表之间的逻辑关系,选择正确的JOIN类型,处理分组后的过滤条件,还可能要用到EXISTS或IN这类关联子查询。这个阶段是刷题的主力区,能覆盖大多数面试和真实业务场景。实际开发中最常写的SQL,也基本都落在这个范围里。

困难题则是把难度拉满,出现频率最高的就是窗口函数和复杂关联。排名问题、连续出现N次的问题、分组内取TopN、累计求和、同比环比、多维度交叉统计,这些都是算法题里的动态规划在SQL世界的映射。解决它们需要对窗口函数有非常扎实的理解,同时对逻辑拆解能力要求很高。

1.2 一条清晰的刷题路线

刚开始刷的时候,我犯过一个错误:东一榔头西一棒子。今天做一道中等题,明天又跳去挑战困难题,结果就是每道题都写了很久,还经常卡住,挫败感很强。后来我按照一个固定的顺序重新把题库过了一遍,效率一下子高了很多。

我的建议是这样分阶段来:

第一个阶段先把所有简单题刷完,目的是熟悉题型和常见的表结构设计。这个阶段不用追求速度,但要保证每一道题都能解释清楚为什么这么写,尤其是WHERE和HAVING的区别、LEFT JOIN和INNER JOIN的区别这类基础概念,一定要在代码之外形成肌肉记忆。

第二个阶段刷带JOIN和聚合的中等题。刷的时候不要急着看题解,先自己在纸上画出表之间的关系,写出需要哪些字段、按什么条件关联、结果要保留哪些行,然后再动手写SQL。因为多表关联的思维方式和单表完全不同,不是把语法背下来就会的。

第三个阶段集中攻克窗口函数相关的困难题。窗口函数这一块建议先花一两个小时专门学一下,把ROW_NUMBER()、RANK()、DENSE_RANK()、SUM() OVER()、LAG()、LEAD()这些函数的区别和写法吃透,再去刷题就会顺畅很多。

另外我也很建议你们关注一下LeetCode的热门100题列表,因为它是综合了算法和数据库两个分类里面被做得最多、评论区最活跃的题目,质量很稳定。把这份列表里的数据库题挑出来优先刷完,性价比非常高,因为这些题在面试中出现过的概率也最高。

2. 刷题前必须先吃透的几个核心SQL知识点

2.1 JOIN到底该怎么理解

数据库题里最常出现的就是多张表的关联查询。很多新手一开始搞不明白INNER JOIN和LEFT JOIN的区别,其实用一个生活里例子就很好懂。

假设你有两张表:一张是班级花名册,记录了所有同学的名字;一张是体检表,记录部分同学的视力情况。如果你想知道“哪些同学有体检记录”,用INNER JOIN,把花名册和体检表按学号关联起来,出来的就是两边都有人,没有体检记录的同学会被自动丢掉。如果你想知道“每个同学的视力情况,没有体检过的同学也保留”,那就必须用LEFT JOIN或者RIGHT JOIN,以花名册为主表,里面的每个同学都会被保留下来,体检表里没有的记录就用NULL填充。

在实际刷题和做业务开发的时候,需要注意一个最经典的坑:把LEFT JOIN写成了INNER JOIN的效果。这个问题的根源在于ON条件写得不对。原理是这样的,LEFT JOIN会保留左表的所有记录,但如果右表有字段在WHERE子句中被过滤了,比如WHERE right_table.score > 60,那么匹配不上的左边记录会因为右边是NULL而同样被过滤掉,最终效果和INNER JOIN一模一样。正确做法是把过滤条件放在ON子句里,也就是写在关联的同时进行限制。这个细节非常容易被忽视,我在后面常见问题的章节会再提一次。

2.2 聚合函数与分组统计

聚合函数是数据库题里的常客,COUNT、SUM、AVG、MAX、MIN这几个必须倒背如流。真正需要仔细体会的是GROUP BY和HAVING的使用时机。

GROUP BY的作用是“把同一类别的记录压缩成一行”,比如要按部门统计人数,就GROUP BY部门编号,然后SELECT部门编号, COUNT(*)。这里需要特别注意,在标准SQL中,SELECT后面出现的非聚合列,必须出现在GROUP BY子句里。例如SELECT department_id, employee_name FROM employee GROUP BY department_id,这里的employee_name就会报错,因为每个分组里有多条员工记录,数据库不知道该展示哪一条。MySQL在默认配置下可能会放行这种写法,但结果具有随机性,实际开发中千万不要依赖这个行为。

HAVING和WHERE的区别是另一道送命题。WHERE是在分组之前对每一行进行过滤,HAVING是在分组之后对每个组进行过滤。你如果想“找出平均成绩大于80分的班级”,这个条件必须用HAVING AVG(score) > 80,因为平均分只能在分组之后才能算出来。如果用WHERE AVG(score) > 80,数据库会直接报错或者得不到正确结果,因为WHERE在执行的时候分组还不存在。

刷题时还有一个常见做法值得记住:如果你既要对行进行过滤,又要对组进行过滤,可以先用WHERE过滤原始行,再用HAVING过滤分组结果。比如“找出2020年以后创建的、平均工资高于5000的部门”,先WHERE create_time > 2020-01-01,再GROUP BY之后HAVING AVG(salary) > 5000,两个条件各司其职,逻辑清晰很多。

2.3 窗口函数:解决困难题的核心武器

窗口函数是解锁数据库困难题的钥匙。它的核心思想是:让每一行数据在保留自身信息的同事,可以看到它所属分组的一些统计信息,相当于在一张表的每一行旁边额外挂一列计算结果。

最常用的三个排序窗口函数是ROW_NUMBER()、RANK()、DENSE_RANK(),它们都跟着一个OVER子句。三者的区别在于并列排名的处理方式:ROW_NUMBER()即使遇到并列也强行分出名次,比如两个第二名之后直接是第三名;RANK()遇到并列会跳过后续名次,两个第二名之后跳过了第三名直接是第四名;DENSE_RANK()遇到并列不跳号,两个第二名之后接着还是第三名。在“分组内取前几名”这类题目里,这三个函数的选择直接决定最终结果。

我看过一道很典型的题:按部门分组,取每个部门工资最高的员工。如果用兼容旧版本的写法,需要先找到每个部门的最高工资,再回到原表去JOIN;如果用了窗口函数,直接SELECT department_id, employee_name, salary, ROW_NUMBER() OVER(PARTITION BY department_id ORDER BY salary DESC) AS rn,然后在外层把rn = 1的记录筛出来,一条SQL就搞定,逻辑简单很多。

另外SUM()、AVG()等聚合函数也可以做窗口函数来用,这时它们的区别就很明显了:普通聚合会把多行压缩成一行,窗口聚合则不会减少行数,而是把聚合结果复制到每行上。这个特性在算累计求和、移动平均、同环比的时候特别有用。

2.4 子查询:不要怕嵌套

子查询的本质就是“在一个查询的结果上再继续做查询”,类似剥洋葱。它分为相关子查询和不相关子查询两种,不相关子查询先独立执行,比如查出平均工资,再把这个结果当作外层查询的条件;相关子查询则要在外层每走一行时都要执行一次,比如“找出比本部门平均工资高的员工”,每次都需要根据当前行的部门重新计算平均工资。

刷题的时候我建议不要一上来就写特别复杂的嵌套,而是先写一个中间查询,把部分结果作为临时表,在外层再用JOIN或者WHERE条件把它们拼在一起。这种写法虽然看起来多了一层,但思路清晰,调试也方便。很多三年经验的开发者在实际工作中也是这么干的,先把复杂的逻辑拆成子查询,确认识结果正确之后再优化合并。

2.5 工具选择:在哪写SQL题

LeetCode官网的数据库编辑器默认支持MySQL,也需要选择版本,比如MySQL 8.0。如果你用了窗口函数,就必须要选8.0以上版本,因为窗口函数是8.0才加的。我自己在本地也装了MySQL,用Navicat作为日常调试工具,遇到重点题我会把题目里的表结构和样例数据导入本地库,断点式地一步步观察中间结果。这种本地复现方式虽然比网页编辑器多几个步骤,但对理解题目的帮助非常大,因为你可以在每一步执行完SELECT * FROM (中间结果)看一下数据长什么样,网页编辑器里做这个操作很别扭。

还有一些人会问要不要装Oracle或者其他数据库来练,我的看法是没必要。LeetCode的判题逻辑和主流数据库语法基本兼容,而你练SQL最重要的是练思路,不是练某个数据库的私货特性。把MySQL玩熟,再迁移到其他数据库只是语法差异的问题,本质上不会特别痛苦。

3. 典型真题实操:从简单到困难的完整拆解

3.1 简单题:员工奖金问题

这是LeetCode第577题,核心是要取出所有员工及其对应的奖金,没有奖金的员工也要显示,且奖金为NULL。

表结构大概是这样的:

CREATE TABLE Employee ( empId INT PRIMARY KEY, name VARCHAR(255), supervisor INT, salary INT ); CREATE TABLE Bonus ( empId INT PRIMARY KEY, bonus INT );

需求描述:返回每个员工的姓名和奖金数额,如果一个员工没有奖金,则奖金显示为NULL。

能直接把这道题做对,基本上就理解了LEFT JOIN的本质。我给出的解法:

SELECT e.name, b.bonus FROM Employee e LEFT JOIN Bonus b ON e.empId = b.empId WHERE b.bonus < 1000 OR b.bonus IS NULL;

注意这里因为要求“没有奖金的员工也要显示”,必须使用LEFT JOIN,以员工为主表。而过滤条件必须带上b.bonus IS NULL,很多人会漏掉这个条件,结果把没有奖金的员工全部过滤掉了。另外,过滤条件要写成b.bonus < 1000 OR b.bonus IS NULL,不能只写b.bonus < 1000,因为NULL参与比较时结果是UNKNOWN,会被过滤掉。

这道题还有一个衍生问法:查询奖金小于1000的员工,但空奖金不算小于1000。那就把OR b.bonus IS NULL去掉即可。同一个题目换个条件,SQL写法完全不同,这个敏感性要培养起来。

3.2 中等题:第二高薪水

这是第176题,一道非常经典的入门中等题。表Employee包含id和salary两个字段,需要找出第二高的薪水,如果没有第二高薪水则返回NULL。

表结构和数据示例:

CREATE TABLE Employee ( id INT PRIMARY KEY, salary INT ); INSERT INTO Employee VALUES (1, 100), (2, 200), (3, 300);

第二高薪水的意思是不包括最高薪水的最大值。一个常见的思路是:先按薪水排序,跳过第一名取第二行。对应的SQL:

SELECT IFNULL( (SELECT DISTINCT salary FROM Employee ORDER BY salary DESC LIMIT 1 OFFSET 1), NULL ) AS SecondHighestSalary;

这里的IFNULL作用是:当子查询查不到任何记录时(比如表中只有一行数据),返回NULL,而不是报错。ORDER BY salary DESC把最大的薪水排在最前,LIMIT 1 OFFSET 1表示从第二条开始取一条。DISTINCT是为了去除重复薪水,否则如果最高薪水和第二高薪水相同,结果就不对了。

这个题还有一种写法是自连接或者相关子查询,比如计算比当前薪水大的数据有多少个,数量为1就是第二高。但LIMIT OFFSET的写法在面试中更直接,也更符合大多数人的阅读习惯。面试官如果想考察窗口函数,会把题改成“找出每个部门的第二高薪水”,这时候就要用DENSE_RANK或者ROW_NUMBER,配合PARTITION BY部门分组。

3.3 中等题:部门工资最高的员工

这是第184题,也是网络热词里经常出现的“部门最高薪水”问题。表Employee包含id、name、salary、departmentId,表Department包含id和name。需求是找出每个部门工资最高的员工。注意这里可能出现并列最高,所以不能用简单的MAX。

表结构:

CREATE TABLE Employee ( id INT PRIMARY KEY, name VARCHAR(255), salary INT, departmentId INT ); CREATE TABLE Department ( id INT PRIMARY KEY, name VARCHAR(255) );

第一种解法,使用窗口函数。按部门分组,对工资进行排名,取排名为1的所有记录:

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

这里用DENSE_RANK而不是ROW_NUMBER,是因为并列最高工资时两个人都应该输出。如果用ROW_NUMBER,并列的人只保留一个,就会漏数据。这又是一个需要仔细辨别函数用法的典型场景。

第二种解法,用GROUP BY和JOIN。思路是先找出每个部门的最高工资,再把这个结果作为临时表,跟原始表关联找出对应的人:

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

第二种方法虽然比窗口函数多了一次子查询,但避免使用窗口函数,在MySQL 5.7及以下版本也能跑,也是面试中经常被期待的输出方式。

3.4 困难题:连续出现的数字

这是第180题。表Logs只有一列num,需要找出所有至少连续出现三次的数字。

表结构:

CREATE TABLE Logs ( id INT PRIMARY KEY AUTO_INCREMENT, num INT );

这里“连续”指的是id连续且num相同。最直接的思路是多表自连接,把Logs表自身关联三次,条件是id分别为当前行、下一行、下下行,同时num都相等:

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

这个解法的核心是理解自连接:同一张表假设成三个不同的别名,通过id之间的偏移关系模拟“相邻的三行”。如果要求连续出现N次,就自连接N次,条件改成id相差N-1。这种思路非常符合业务里的“连续签到N天”问题。

窗口函数的解法也值得掌握。连续出现的本质是num相同,而id递增,那么可以在分组内部安排一个行号,然后看num的值和行号之差,如果差值相同,说明这几行是连续的:

SELECT DISTINCT num AS ConsecutiveNums FROM ( SELECT id, num, id - ROW_NUMBER() OVER(PARTITION BY num ORDER BY id) AS diff FROM Logs ) t GROUP BY num, diff HAVING COUNT(*) >= 3;

这里的技巧比较巧妙,ROWNUMBER在相同的num内按id排序生成连续的序号,如果id本身就是连续的,则id减去行号是一个固定值;一旦出现断档,差值就会变化。然后按num和diff分组,数量达到3说明存在连续三次的数据。这道题充分体现了窗口函数在处理“连续性”问题上的威力,面试中如果能在白纸上推导出这个解法,加分效果非常明显。

3.5 困难题:游戏玩法分析

这道题是LeetCode第534题或第550题的不同变体,核心是玩家在一个游戏中的首次登录时间和后续活动分析。很多数据库课程设计里也有类似需求,比如用户首次访问网站的时间、每个用户最后一次下单日期、连续活跃天数等。

表结构:

CREATE TABLE Activity ( player_id INT, device_id INT, event_date DATE, games_played INT, PRIMARY KEY (player_id, event_date) );

比如需求是找出每个玩家的首次登录日期。最简单的写法是:

SELECT player_id, MIN(event_date) AS first_login FROM Activity GROUP BY player_id;

这个看起来很简单,但真正有难度的是把首次登录日期作为过滤条件,再去查询首次登录后一天是否也登录了,这就涉及到把MIN(event_date)的结果当作子查询,跟原表做关联:

SELECT a.player_id, a.event_date AS login_date, a.games_played FROM Activity a JOIN ( SELECT player_id, MIN(event_date) AS first_day FROM Activity GROUP BY player_id ) t ON a.player_id = t.player_id AND a.event_date = DATE_ADD(t.first_day, INTERVAL 1 DAY);

这类进阶写法完美展示了聚合查询和子查询的组合。先算出每个玩家首次登录日期,再通过日期运算判断次日仍在活跃,这种模式在真实的用户留存分析场景里非常常见,做运营数据报表时经常要用到。

在这道题上我还遇到过一种情况:玩家在同一天有多次活动记录。如果主键定义是(player_id, event_date),同一天只有一条记录,问题不大;如果允许多条,那么需要先对player_id和event_date去重,再用去重后的结果作为基础数据继续查询,否则统计出来的活跃人数会虚高。这个预处理细节很值得写在笔记里,面试时主动提出来能体现你的细心程度。

4. 从刷题延伸到面试和课程设计

4.1 刷题之外的面试必问:索引、事务、死锁

LeetCode数据库题能锻炼你的SQL编写能力,但数据库面试还经常涉及另一堆概念性内容,比如索引、事务、隔离级别、死锁。这些知识在在线判题系统里很难直接体现,但面试官几乎必问。

索引那部分,最核心的问题是为什么用B+树而不是哈希表或者链表。B+树是矮胖型多路搜索树,数据都存在叶子节点,叶子节点之间用指针连成有序链表,所以既支持快速单行查询,也支持范围查询和排序。哈希索引虽然单行查询更快,但遇到范围查询、排序、模糊匹配就会退化。这个知识点建议配合EXPLAIN在本地库多观察几次,眼见为实。

死锁则是另一个高频问题。死锁的本质是两个或多个事务各自持有一些资源,同时又在等待对方释放资源,形成一个循环等待。经典案例是事务A更新表1后去更新表2,事务B更新表2后去更新表1,两边谁也不让谁,最后数据库检测到死锁会强制回滚其中一个事务。解决死锁的常规策略是保证事务以固定的顺序访问资源,以及合理控制事务的持锁时间。

事务的隔离级别也要重点理解,读未提交、读已提交、可重复读、串行化这四级,每一级解决什么问题、带来什么新问题,都需要能用自己的话讲清楚。MySQL默认是可重复读,Oracle默认是读已提交,这个考点经常出现。

4.2 刷题之外的性能优化思维

LeetCode判题只关心结果正确,但真实业务里SQL性能同样重要。面试官很喜欢让候选人分析一条慢SQL的优化方案,所以平时刷题时也要渐渐养成姿势好习惯。

尽量不用SELECT *,只查询需要的字段,不仅能减少网络传输量,还能让索引覆盖查询变得更可能。多表关联时,尽量让小表驱动大表,在MySQL里可以通过STRAIGHT_JOIN或者调整子查询顺序来影响优化器的执行计划。WHERE条件里避免对字段做函数运算,例如WHERE YEAR(create_time) = 2024,这个写法会导致索引失效,正确写法是WHERE create_time >= 2024-01-01 AND create_time < 2025-01-01。

还有一个容易被忽略的重点:连表查询时连接列的数据类型要保持一致。如果一边是INT,一边是VARCHAR,MySQL会做隐式类型转换,可能导致索引失效,全表扫描。我在一道LeetCode题里就碰到过类似情况,本地复现的时候把关联字段从VARCHAR改成INT后,执行时间从毫秒级降到了微秒级。

4.3 数据库课程设计怎么利用这些题

很多学生朋友在课程设计阶段拿到的是类似学生选课系统、图书管理系统、电商订单系统这类题目,这些系统里涉及的SQL操作,很大概率都能在LeetCode数据库题里找到原型。

学生选课系统要写“每个学生的平均成绩”“每门课的最高分和选课人数”“没有选课的学生名单”,这些对应的是聚合、JOIN、NOT EXISTS。图书管理系统要查“借阅次数最多的书籍TOP10”“超过三个月未还书的读者”,这些对应的是排序、日期函数、窗口函数。把这些SQL写熟练,课程设计里的核心功能基本就搞定一大半了。

另外在网上有很多现成的练习用数据库,比如北风数据库、经典的学生-课程-成绩三表结构,这些库的数据量很适合做SQL练习。用Navicat导入后,自己给自己出题,模拟业务场景,比单纯刷题更接近实战,也更能锻炼业务理解能力。遇到“数据库课程设计”不知道选什么题目的朋友,我很推荐拿一道中等难度的LeetCode题稍微改一下表结构,扩成一个小系统,比如把“部门工资最高的员工”扩展成“各部门薪资排行报表管理系统”,既有难度亮点,又能覆盖很多知识点。

4.4 关于周赛和刷题节奏的个人观察

LeetCode每周都有周赛,里面算法题是主力,偶尔也会穿插数据库题。热词里出现的“leetcode周赛430”就是一个典型例子。如果你的重心目标是数据库,我不太建议每周都死磕周赛里那种限时高难度的算法题,因为这和数据库SQL题的核心考法不一样。数据库题更看重准确性和严谨性,算法题更看重速度。但每周的周赛题目中一旦出现Database题,其实是很值得去做的,因为它们通常代表了题目设计的一个新方向,做完以后把题解汇总起来,能形成很好的素材库。

刷题节奏上,我个人的体会是细水长流比突击有效得多。每天固定两小时刷一两道数据库题,坚持三个月,比周末刷十个小时更容易形成长时记忆。LeetCode有每日一题和打卡机制,建议用起来,而且把打卡界面当作自己的成就墙,看到连击天数以后会有一种舍不得断签的心理,反而成了坚持的动力。

5. 常见错误与排查技巧实录

5.1 高频错误速查表

以下这些错误是我自己在刷题和带新人时遇到最多的情况,整理成一张表,方便你对照自查:

错误类型错误写法正确写法原因分析
NULL值被过滤WHERE salary > 1000WHERE salary > 1000 OR salary IS NULLNULL参与比较时结果为UNKNOWN
LEFT JOIN变成INNER JOINWHERE right_table.score > 60把条件放ON子句WHERE中的NULL过滤掉左表保留行
GROUP BY字段遗漏SELECT dept_id, name, AVG(salary) GROUP BY dept_idSELECT dept_id, AVG(salary) GROUP BY dept_id非聚合列必须出现在GROUP BY
排名函数选错并列第一时用ROW_NUMBER并列第一时改DENSE_RANKROW_NUMBER不给并列留位置
窗口函数版本不支持在MySQL 5.7用ROW_NUMBER换MySQL 8.0或改子查询窗口函数是8.0新特性
ORDER BY / LIMIT顺序LIMIT 1 OFFSET 0 ORDER BY salary DESCORDER BY salary DESC LIMIT 1 OFFSET 0ORDER BY必须先排序再取数
去重方式错误用SELECT num用SELECT DISTINCT num多行重复会输出重复结果
日期函数不会用DATE_ADD不会,手动字符串拼接DATE_ADD(event_date, INTERVAL 1 DAY)日期运算要使用内置函数

5.2 调试SQL的土办法

在线判题系统报错以后,最烦的事情是不知道数据到底长什么样。我的土办法是先在本地建一个一模一样的表结构和样例数据,把SQL的各个部分拆开跑,一层层看中间结果。

举一个真实案例。我刷一道“每个产品在2020和2021年都销售”的题时,先写了子查询统计每年的销售情况,结果发现2020年有记录的产品在2021年也有记录,但最终输出却少了一部分。我用本地库逐步排查,先跑最内层的子查询看每年销售结果,发现因为表中销售日期是DATETIME类型,我直接用YEAR(sale_date) = 2020,但数据里只有一条记录是2020-01-01 00:00:00,其他都是NULL。问题根源在于创建表时日期字段有大量NULL,YEAR(NULL)返回NULL,判断自然是NULL。这个坑在题目描述里几乎不会明说,只有去数据库里查一遍才能发现。

所以调试SQL的时候,我的习惯是三步走:第一步先跑最内层,确认识子查询结果符合预期;第二步把子查询结果作为临时表,跟外层表JOIN,逐层往上检查;第三步最后加最终条件,比如HAVING、ORDER BY、LIMIT。每加一个条件就重新看一遍结果,不要等整条SQL写完再一起调。

5.3 三道错题复盘

最后复盘三道我印象最深的错题,都是简单的错误但后果很严重。

第一道是“第二高薪水”,我第一次写的时候忘了DISTINCT,结果在数据里有多个相同工资时输出错误。复盘的时候意识到,题目里的“第二高”不是物理上的第二条记录,而是逻辑上的第二个不同的工资值,所以去重是必须的。

第二道是“删除重复邮箱”,这道题要求保留每个邮箱的最小id记录。我一开始用GROUP BY email取MIN(id),然后想把非最小id的记录删掉,但DELETE和子查询在同一张表上的处理方式很考验写法和数据库版本。在MySQL里不能直接在同一个表的DELETE语句中同时SELECT同一张表,需要先通过派生表包一层。这个限制如果我当时不知道,就会一直在报错里打转。

第三道是“大洲和国家”这类带多表关联的题,我忘了考虑关联字段的索引。本地数据量小所以看不出问题,但面试官会追问“如果你的表有十万行怎么办”,这时候如果没有给关联字段建索引,连接查询会慢得难以接受。所以后来我刷题的时候,都会顺手用EXPLAIN看一下执行计划,看看有没有全表扫描,这也是区分普通工程师和资深工程师的一个细节。

6. 一点后续可以扩展的方向

数据库题库刷完之后,自然延伸的方向其实很多。后来我自己经常碰到“数据库同步”“向量数据库”这类话题,前者是因为公司要做主从同步和数据迁移,后者是AI相关的项目需要做相似度检索。这两个方向都会用到大量SQL知识,但又各有各的新体系。

如果你对“数据库同步软件”感兴趣,可以从理解binlog和主从复制的原理开始。MySQL的主从复制本质上是把主库的二进制日志同步到从库并重放,中间涉及同步延迟、断点续传、主键冲突等问题。这个方向不适合在LeetCode上刷,适合搭建两个本地MySQL实例,做一次主从配置,亲手观察同步过程。

向量数据库则更偏向于处理非结构化数据,核心是向量索引和相似度计算,语法和传统关系型数据库差异很大。不过向量数据库的底层很多也借鉴了数据管理的思想,比如分片、副本、索引结构,所以传统数据库基础越扎实,学起来就越轻松。

我个人觉得,把LeetCode数据库题刷完并不是终点,它更像是一张通行证,让你有了足够的SQL能力去面对真实世界的各种数据问题。但核心还是要慢下来写每一行SQL,去思考数据库为什么要这样设计,去体会数据之间的关系。

我知道数据库这份合集对很多人来说没有算法题那么刺激,但它却最接近真实业务里每天要面对的活。你能把这些题做到看一眼就能写出干净SQL,到哪儿都饿不着,这句话放在当下依然不过时。

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

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

立即咨询