ARTICLE DETAIL

资讯详情

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

50道SQL练习题:从多表连接到窗口函数,吃透面试高频考点

50道SQL练习题:从多表连接到窗口函数,吃透面试高频考点 说实话我见过太多人学SQL的方式是错的。买了一堆书、收藏了一堆教程打开软件却写不出一条能跑的查询。你问他连表查询会用吗他说会你让他现场写一条“查每门课成绩最高的学生”这种题他当场卡住。SQL这东西知识点就那么多真正拉开差距的是你动手写过多少、踩过多少坑。这份“50道SQL练习题”就是冲着这个问题来的。它不是一堆零散问题的大杂烩而是按知识点密度和难度梯度排过的题单覆盖了多表连接、分组聚合、子查询、窗口函数、去重空值处理这些SQL面试和日常开发的高频考点。这篇文章我不打算把50道题全部贴一遍——那样反而让你懒得思考——而是把题目体系的设计思路、核心考点的拆解方法、还有做题时最容易踩的坑全部讲透并挑几道最有代表性的题带你把完整解题过程过一遍。不管你是准备面试的开发者还是刚从教程里出来不知道怎么上手的新手照着这个思路练完效果会非常明显。1. 题目体系的设计思路与拆解1.1 为什么是50道而不是20道或者100道先说个实情我最初整理这套题的时候其实是从20道开始的。练到后面发现覆盖不全——窗口函数还没练、自连接没涉及、行列转换这种变态题也没放进去。后来加到了50道才算把基础语法、中级查询、高级分析这三大块都兜住了同时又把重复度压到了最低。50道题的数量是有讲究的。20道太少很多知识点只能浅尝辄止像GROUP BY配合HAVING的坑、NULL值处理这种细节不靠题量堆是记不住的100道又太多大部分人根本刷不完刷到30道就开始疲了导致后面全是无效重复。50道这个量恰好能在两周内练完每天抽出40分钟到1小时做3到4道周末集中攻克难题节奏正好。从知识点覆盖上来看这套题大致是按下面这个比例分配的知识点板块题目数量难度区间对应场景基础查询与条件过滤8道入门WHERE、ORDER BY、LIKE、IN、BETWEEN聚合与分组统计12道入门到进阶GROUP BY、HAVING、COUNT/SUM/AVG多表连接与子查询15道进阶JOIN、自连接、EXISTS、IN子查询窗口函数与排名8道高阶ROW_NUMBER、RANK、SUM OVER 等去重、空值、时间与综合7道进阶到高阶DISTINCT、NULLIF、日期函数、综合练习这个比例不是拍脑袋定的。关系型数据库面试的核心无非就是“查得出来、查得对、查得快”——前两类题保证你能正确取数第三类题是业务开发里最常用的技能第四类解决的是“分组内排名”“累计求和”这类复杂报表需求最后一类把各种坑集中踩一遍加深记忆。1.2 经典四表模型为什么练习SQL要有一份稳定的数据这套50题用的是经典的“学生-课程-成绩-教师”四表模型。为什么要用这个模型因为它足够简单四张表之间的关联关系清晰而且高度贴合真实业务——学生表对应“用户表”、课程表对应“商品/品类表”、成绩表对应“订单/流水表”你在工作中遇到的绝大多数查询本质上都是在处理这种“主表明细表维度表”的关系。四张表的建表语句我直接放出来你可以一次性建好后面做题反复用-- 学生表 CREATE TABLE Student ( SId VARCHAR(10), Sname VARCHAR(20), Sage DATETIME, Ssex VARCHAR(10) ); -- 课程表 CREATE TABLE Course ( CId VARCHAR(10), Cname VARCHAR(20), TId VARCHAR(10) ); -- 教师表 CREATE TABLE Teacher ( TId VARCHAR(10), Tname VARCHAR(20) ); -- 成绩表 CREATE TABLE SC ( SId VARCHAR(10), CId VARCHAR(10), score DECIMAL(5,2) );注意这里的几个细节Sage用的是DATETIME而不是单纯的年份就是为了让你在练习时接触到时间函数score用的是DECIMAL而不是INT是为了保留小数位方便后续AVG这类聚合运算。成绩表SC是典型的关联表里面只存ID和分数和实际业务里的订单表逻辑一样。我见过有人为了“省事”把所有数据放在一张大宽表里练这其实是个坏习惯。真实业务几乎不会给你一张现成的宽表绝大多数时候你需要自己JOIN出宽表。保持多表结构练习才能锻炼出“看到需求就想到关联路径”的肌肉记忆。1.3 不同基础的人应该怎么练这套题新手最容易犯的错就是从头开始一题一题按顺序刷遇到不会的就开始挠头挠不出来就看答案看完觉得自己会了合上屏幕又不会了。正确的打开方式是按阶段来如果你的SQL基础比较薄弱我建议先跳过带“排名”“累计”字样的窗口函数题把前20道基础题做扎实了再回头啃。基础题的正确标准不是“写出了就行”而是“不查资料就能写出来”。我自己的经验是一道题如果当天做完、第二天还能凭记忆重写一遍才算真正掌握。如果你已经有一定经验、主要是为了面试突击可以直接从第20题之后开始做但每道题都要问自己三个问题这个查询用到的关键函数是什么还有没有别的写法哪个写法在数据量大时性能更好比如同样是“查每门课成绩最高的学生”子查询、窗口函数、自连接三种写法都能实现但跑在百万级数据上性能会差出好几倍。这个问题在后面第4部分我会详细展开。2. 核心考点拆解与答题思路2.1 多表连接INNER JOIN、LEFT JOIN 和自连接到底怎么选多表连接是SQL里最核心也最容易出错的考点。50道题里涉及连接的至少有一半而连接题里最大的坑不是语法不会写而是“用错连接类型导致结果比预期多或比预期少”。我讲讲最常见的场景。比如有一道题是“查询所有学生的学号、姓名、选课数”这个需求的关键词是“所有学生”。只要出现“所有”就意味着有的学生可能没有选任何课如果成绩表里压根没有他的记录用INNER JOIN就会把这个学生丢掉结果自然不对。这时候必须用LEFT JOIN把学生表放在左边作为驱动表。但如果你用的是LEFT JOIN第二个坑马上会冒出来COUNT函数的计数对象。我见过太多人写COUNT(SC.CId)和COUNT(*)结果对不上的情况。左连接之后没选课的学生在成绩表对应的列全是NULLCOUNT(SC.CId)只统计非NULL的行所以结果是0而COUNT(*)统计的是连接后的所有行结果是1。这里一定要记住统计子表的行数永远用子表的字段去COUNT不要用COUNT(*)。自连接是另一个让新手头疼的东西。比如“查询每门课成绩不低于该课程平均分的学生”——你需要把同一张成绩表当作两张表来用一张取学生成绩一张计算平均分。我第一次练这个连接时脑子绕不过弯后来我用了个土办法把同一张表想象成两份独立的打印件放在桌上一份用于查明细一份用于聚合计算写SQL时就写上不同的别名这样就好理解多了。2.2 分组聚合GROUP BY 和 HAVING 的经典陷阱分组聚合在练习题里占的比重最大也是日常工作里最常用的功能但坑也最多。最经典的一个坑就是“GROUP BY之后SELECT了没被分组的列”——这在MySQL里甚至能跑出结果但那一列的值是随机的、毫无意义在SQL Server或PostgreSQL里直接报错。我见过不止一个开发同事因为这个“随手写”的查询在报表里查出了对不上的数据。正确的聚合查询只有两类GROUP BY后面跟着的列或者被聚合函数包起来的列。其他任何裸列都不要出现在SELECT里这不是语法限制而是逻辑保证。HAVING的坑则在于很多人分不清它和WHERE的区别。一句话记住WHERE在分组之前过滤行HAVING在分组之后过滤组。写“查询平均分大于60分的学生”这种题时逻辑上必须先按学生分组、计算平均分然后才能把平均分大于60的筛选出来所以只能用HAVING而“查询2024年入学的学生”这种条件在分组之前就能过滤掉用WHERE是最优选择——因为WHERE过滤掉的行不参与分组计算顺带还能减少聚合的开销。2.3 去重与空值DISTINCT 和 NULL 的那些坑去重是SQL入门就学的东西但题目里真正想考的是“用对地方”。有一个常见的误区是认为COUNT(DISTINCT 列名)和先DISTINCT再COUNT结果一样——确实一样但前者在数据量大时会有严重的性能问题因为DISTINCT本质上是排序去重消耗很大。更高效的做法是先用子查询把要去重的集合缩小再在外层做聚合。这套练习题里专门有一道“统计每门课的选课人数一个学生同一门课只算一次”考的就是这个逻辑。NULL值的处理更是SQL里永恒的坑。举个最典型的例子WHERE 字段 ! A这个查询查不出字段为NULL的行——因为NULL和任何值比较都是UNKNOWNWHERE只保留结果为TRUE的行。50道题里专门设计了“查询没有参加任何考试的学生”“统计成绩为空的学生人数”这类题目的就是把IS NULL、IS NOT NULL、IFNULL、COALESCE这类处理手法练熟。我在实际开发中还踩过一个更隐蔽的坑用COUNT(列名)统计某列非空数量时如果列里有NULL统计结果会自动把它们剔除。这个特性有时候是好事有时候却会掩盖数据质量问题——比如统计“订单金额大于100的订单数”时如果部分订单金额字段是NULL这单就会被悄悄漏掉而业务方根本不知道。所以每次写完带COUNT和NULL判断的SQL最好像强迫症一样检查一遍逻辑覆盖的范围。2.4 窗口函数解决“分组内排名”“累计求和”这类问题的利器窗口函数是近几年面试的高频考点也是从“会写SQL”到“写得优雅”的分水岭。这套题里至少有8道题涉及窗口函数其中最常考的是三兄弟ROW_NUMBER、RANK、DENSE_RANK。它们的区别必须刻在脑子里ROW_NUMBER无脑给每一行分配一个连续不重复的编号RANK遇到相同值会并列但下一个名次会跳过比如1、1、3DENSE_RANK遇到相同值也并列但下一个名次不跳过比如1、1、2。我见过有人面试时在这三个函数上翻车的——题目没变只是数据里恰好有并列分数他想当然以为排名永远是连续的。比如经典题“按课程分组查询每门课程成绩排名第一的学生”用窗口函数是最直接的写法SELECT CId, SId, score FROM ( SELECT CId, SId, score, ROW_NUMBER() OVER (PARTITION BY CId ORDER BY score DESC) AS rn FROM SC ) t WHERE rn 1;这段SQL的逻辑分三步先用窗口函数在每门课内部按分数降序编号再在外层过滤出编号为1的行。PARTITION BY就是“分组”的意思但它和GROUP BY最大的不同是窗口函数不会压缩行数每一行都保留自己的编号这样后续还能拿到整个分组的上下文信息。除了排名窗口函数还有一个高频应用是“累计求和”比如“查询每门课程每个学生的分数并计算该学生在本课程中的所有分数排名”——这类问题用自连接写会很痛苦用SUM(score) OVER (PARTITION BY CId ORDER BY score)一行就能搞定。3. 实操过程从建表到完整解题演示3.1 环境准备与模拟数据的生成工欲善其事必先利其器。开始做题前建议先准备一个干净的执行环境。如果你电脑上装了MySQL 8.0或SQL Server 2019以上版本直接用就行如果什么都没装强烈推荐Docker方式拉一个MySQL 8.0镜像两分钟就能跑起来还不会把系统搞乱。事务和数据部分我建议不要手工一行行往表里插写个脚本来生成模拟数据更高效。下面是我用来生成学生表和成绩表的常用写法你直接抄走就能用-- 生成1000个学生这里只示例几个 INSERT INTO Student (SId, Sname, Sage, Ssex) VALUES (01, 赵雷, 1990-01-01, 男), (02, 钱电, 1990-12-21, 男), (03, 孙风, 1990-05-20, 男), (04, 李云, 1990-08-06, 男), (05, 周梅, 1991-12-01, 女); -- 生成成绩数据时可以用笛卡尔积快速填充 INSERT INTO SC (SId, CId, score) SELECT s.SId, c.CId, ROUND(50 RAND() * 50, 2) -- 随机生成50到100之间的分数 FROM Student s CROSS JOIN Course c WHERE RAND() 0.2; -- 留出20%的空选模拟真实情况这里有个细节我用ROUND(50 RAND() * 50, 2)生成随机分数这比手工写死分数实用得多——数据量一大你才能体会到分组聚合和连接查询在真实数据量下的性能和结果差异。另外我在插入成绩时故意留了20%的缺考记录就是为了让后面“查没选课的学生”“处理NULL值”这类题目有数据可查。真实业务的数据永远是“脏”的练习时就应该用带坑的数据。3.2 五道代表性题目的完整拆解下面我从50道题里挑5道最有代表性的把思考过程和写法完整过一遍你可以看完之后举一反三。第1题查询“01”课程比“02”课程成绩高的所有学生的学号。这道题看似简单其实考的是“同表多行转列”的思路——你需要把同一个学生的两门课成绩放到同一行才能比较而成绩表里每个学生每门课是单独一行。解法是用自连接或两个子查询分别取出两门课成绩后再JOIN。经典写法SELECT a.SId FROM SC a JOIN SC b ON a.SId b.SId WHERE a.CId 01 AND b.CId 02 AND a.score b.score;这里把SC表当作两个表用别名a和b分别对应课程01和02的成绩。我最初练这道题时想的就是“怎么把行变成列”后来才明白本质是“同表不同行的字段需要横向比较时就用自连接把多行变成一行”。第2题查询平均成绩大于60分的学生学号和平均成绩。这道题核心是GROUP BY HAVING组合非常经典。写法如下SELECT SId, AVG(score) AS avg_score FROM SC GROUP BY SId HAVING AVG(score) 60;有人第一反应会用WHERE写了WHERE AVG(score) 60结果报错。原因之前讲过了WHERE是在分组前执行的它没法知道分组的聚合结果。这里补充一个经验HAVING里写AVG(score)不是必须与SELECT里的聚合函数重复但为了可读性建议保持一致不要一个写AVG一个写SUM排查问题时会很崩溃。第3题查询所有课程成绩小于60分的学生姓名。这道题暗藏了一个逻辑陷阱。“所有课程都小于60分”不等于“最少有一门课小于60分”。如果你写成找成绩小于60的记录那结果会把只挂了一科的人也包含进去。正确逻辑是先找所有课程里最高分都小于60的学生——也就是不存在任何一门课大于等于60分。用NOT EXISTS子查询写清晰又高效SELECT Sname FROM Student s WHERE NOT EXISTS ( SELECT 1 FROM SC sc WHERE sc.SId s.SId AND sc.score 60 );NOT EXISTS的语义是“不存在满足条件的行”顺着这个思路反着想很多“所有”“全部”类型的问题都能转化成NOT EXISTS。这套题里类似的反向思维还有好几道练多了你会发现SQL的逻辑其实很像数学里的逆否命题。第4题查询每门课程成绩排名前三的学生。这道题用窗口函数是最自然的方式SELECT CId, SId, score FROM ( SELECT CId, SId, score, DENSE_RANK() OVER (PARTITION BY CId ORDER BY score DESC) AS rk FROM SC ) t WHERE rk 3;注意这里我用了DENSE_RANK而不是ROW_NUMBER。为什么因为业务需求往往关心“并列前三”——如果第三名有并列ROW_NUMBER会把并列的人拆成第3和第4导致排名结果不完整DENSE_RANK会正确地把并列第三的两个人同时保留。实际开发里做排行榜、TopN报表时这个选择直接影响结果的正确性一定要根据业务语义来选。第5题查询各科成绩的总分排名。这道题综合了聚合函数和窗口函数的配合SELECT CId, SUM(score) AS total_score, RANK() OVER (ORDER BY SUM(score) DESC) AS ranking FROM SC GROUP BY CId;注意窗口函数是在GROUP BY完成之后才计算的所以窗口函数里可以直接用SUM(score)作为排名字段。这算是一道典型的综合题——如果GROUP BY和窗口函数的执行顺序搞混这道题基本写不出来。SQL的执行顺序大概是FROM - WHERE - GROUP BY - HAVING - SELECT - ORDER BY - 窗口函数理解这个顺序对调试复杂查询帮助极大。3.3 从练习题到面试题的迁移能力很多人练习时会陷入一个误区题做完了就完了没有总结。但面试官真正想考察的从来不是“你会不会做这道题”而是“你面对一个从没见过的业务需求时能不能用SQL高效解决”。比如上面第4题“每门课前三名”面试时可能换皮变成“查每个部门工资最高的员工”“查每个品类销量最好的商品”。结构完全一样只是换了表和字段。所以我的建议是每做完一道题停下来花1分钟想一下——这个查询模式还能用在什么业务场景能不能换个表复现一遍我自己的经验是50道题做完后真正内化成能力的是十几种查询模式而不是50个孤立答案。这些模式包括行转列、取分组TopN、累计求和、多条件关联、存在性判断、非空处理等。把这些模式融会贯通遇到任何新需求都能快速匹配到对应的解法。4. 常见问题与排查技巧实录4.1 查询结果和预期不符第一步该查什么做题和实际开发中遇到“结果不对”是常态关键要有一套高效的排查顺序。我自己的排查经验是三步走第一步把SQL拆成小块逐段执行尤其是子查询和JOIN部分先分别跑一遍看中间结果对不对第二步确认关联字段是否有重复值如果关联字段在子表中不是唯一的JOIN结果会成倍膨胀这也是“结果比预期多”的头号原因第三步检查过滤条件中是否存在NULL值数据如果你查“分数小于60”的学生那些成绩为NULL的缺考学生会默默被过滤掉——这往往是“结果比预期少”的元凶。举一个我踩过的具体例子有一次统计“每门课的报名人数”我直接对成绩表做GROUP BY得出的数字比业务方给的少了将近三成。排查了半天才发现问题在于成绩表只存了“已出分”的记录还有一些学生选了课但分数为NULL、根本没进这张表。要拿到真实报名数必须左连接选课表和成绩表再按选课表分组。这个坑就是典型的数据表设计语义和业务语义不一致造成的练习时遇到这类题不要只满足于“能跑通”还要多想一步“统计口径对不对”。4.2 EXPLAIN 怎么看慢SQL优化的核心入口热搜词里“慢sql优化 explain主要看哪些信息”这个关键词特别典型。50道练习题虽然主要目的是“写对”但等你写到后面涉及大数据量的题目时必然会遇到“能跑但跑得慢”的情况。这时候EXPLAIN就是最重要的工具。EXPLAIN输出结果里我最关注的永远是四个东西type列这是访问类型从好到差大致是const - eq_ref - ref - range - index - ALL。看到ALL全表扫描而表数据量又很大时基本可以确定需要加索引了。key列实际用到的索引。如果key是NULL说明没走索引得检查WHERE条件里的列有没有被索引覆盖。rows列预估扫描行数。这个数字越大查询越可能慢。优化目标就是让这个数字尽量小。Extra列如果出现Using filesort文件排序或者Using temporary临时表意味着排序和去重操作占了额外资源数据量在大并发下容易成为瓶颈。我举个例子第4题窗口函数那个查询在外层加了WHERE rk 1如果子查询里没有在(CId, score)上建联合索引EXPLAIN会显示如下id select_type table partitions type possible_keys key rows Extra 1 PRIMARY derived2 ALL NULL NULL 1000 Using where 2 DERIVED SC ALL NULL NULL 1000 Using temporary; Using filesort看到Using temporary; Using filesort就要有警觉了——这说明窗口函数的排序是在临时表里完成的数据量大时会比较吃力。解决办法是给SC表建一个(CId, score)的联合索引让排序直接走索引。4.3 练习题阶段常犯的三个语法细节错误除了逻辑错误练习题阶段还有三个特别常见的语法细节错误我几乎在每一个来求助的人身上都见过第一字符串和数值比较时类型不匹配。比如WHERE Sage 1990如果Sage是DATETIME类型这个写法在部分数据库里能跑但结果诡异在另一些数据库里直接报错。正确做法是用年份函数WHERE YEAR(Sage) 1990。第二别名表名用了中文或保留了空格。比如ORDER BY 平均分 desc这在某些数据库配置下会报错或者引发奇怪的排序结果。正确做法是给别名加反引号或双引号ORDER BY 平均分 DESC。第三分页时ORDER BY字段不唯一导致数据重复或丢失。比如你用LIMIT 10 OFFSET 0和LIMIT 10 OFFSET 10分页但排序的列有重复值MySQL的排序在值相同时是不保证顺序的两页之间可能出现重复记录或漏掉记录。解决办法是在ORDER BY最后加一个唯一字段比如自增ID。说实话这些错误都不是“不会写SQL”造成的而是“没有养成好习惯”造成的。我在做完50道题之后回头看成长最快的不只是会写的语法变多了而是写完SQL之后会自动检查这些细节——这本身就是高级工程师和初级工程师的差别之一。4.4 练习题之外的提醒心法比题量更重要最后再说一点隐藏的“潜规则”。搜SQL相关热词时你一定看到过“SQL注入万能密码绕过”这个词。SQL注入的本质是“用户输入拼接进了SQL语句被当作代码执行了”——比如一个登录框里输入 OR 11如果你的SQL写的是字符串拼接这个输入就会绕过密码校验。作为写SQL的人一定要建立一道底线思维永远不要在前端/后端代码里用字符串拼接的方式写SQL必须使用参数化查询或预编译语句。这不是练习题里能直接考出来的但它是每一个真正在工作中写SQL的人必须刻在脑子里的安全意识。还有一点很多人练完50道题觉得自己“会SQL了”但真实业务里的SQL基本都不会像练习题这样把条件给得明明白白。实际工作中你需要自己判断这个查询该不该开只读事务要不要限制返回行数要不要加索引要不要考虑数据倾斜这些软技能恰恰是练习题之外更需要刻意培养的。所以我更建议把这50道题理解为一个起点——先把底子打牢再往真实场景里扎。根据我自己的实操体会刷完这50道题之后最值得做的事是拿一份真实的脱敏业务数据自己给自己出题比如分析“最近30天各品类的销售趋势”“各区域用户的复购率”这种综合需求。只有这样SQL才会从“题目里的语法”变成“你手里的工具”。
返回列表