最近后台收到不少私信核心问题就一个——SQL到底要怎么练才能既应付面试又扛得住实际项目我的答案始终是两个字刷题。不是零散地刷而是找一套成体系的题像练内功一样把查询逻辑、连接方式、聚合维度、窗口函数这些基本功扎扎实实过一遍。今天想聊的这套50道SQL练习题是我留存最久、也最常拿来考察新人的一套四张表五十个查询场景覆盖了从单表查询到窗口函数的绝大部分核心语法也精准踩中了面试题的高频命题方向。无论你是准备转行数据分析、想巩固后端基本功还是在带团队时不知道用什么题目考核新人这套题的练习价值都值得认真看一眼。接下来我会基于这套题把解题思路、易错点、优化方法一次讲透。1. 为什么这套50题值得刷面试痛点与数据模型设计1.1 从面试翻车现场看SQL内功差距我在参与团队招聘时有个很明显的感觉很多候选人简历上写着“熟练使用SQL”但到了手写题环节就露馅。单表查询基本没问题一旦涉及多表关联就开始凭感觉乱连聚合函数会用但分不清GROUP BY和HAVING各自的职责边界窗口函数更是大部分人都没用过。问题根源往往不是不勤奋而是缺少一套有梯度的训练题。这套50题的价值恰恰在于它的梯度设计。前20道是基础查询练的是SELECT、WHERE、ORDER BY、DISTINCT这些最底层的语法中间20道进到多表连接、子查询、聚合分组练的是把业务问题翻译成查询逻辑的能力最后10道涉及窗口函数、行转列、复杂嵌套练的是高阶写法和性能意识。刷完一轮下来你对SQL的理解会从“能跑通”升级到“跑得对”再进阶到“跑得巧”。1.2 经典四表模型为什么练习都用这几张表这套题的数据模型非常经典凡是用过数据库的朋友基本都见过一张Student表存学生基本信息一张Course表存课程信息一张Teacher表存教师信息再加一张SC表存学生选课成绩。四张表通过主外键互相引用构成了一个简单但五脏俱全的教学管理场景。为什么几乎所有SQL教程和练习题都绕不开这套模型因为它把业务中最常见的关系类型都体现了学生和成绩是一对多课程和教师是多对一学生和课程通过中间表SC形成多对多关联。更重要的是这些表的数据量不大你可以肉眼验证每一步查询结果是否正确。这一点对新手极其重要——在大表上写SQL写错了你根本察觉不到在这套数据里结果对不对自己心里有数报错也容易定位问题到底出在关联条件写错还是聚合维度选错一眼就能看出来。我在给新人推荐这套题时通常会先让他们自己建表、写INSERT语句把数据灌进去而不是直接下载现成的SQL脚本。因为建表这件事本身就是在练基本功字段类型选什么、主键怎么设、外键关联怎么建、数据怎么构造才能覆盖各种边界情况这些都会直接影响后面写查询语句的思路。2. 50道题铺开的SQL能力地图从单表查询到窗口函数2.1 基础查询SELECT、WHERE、ORDER BY与去重这套题的前十几道基本是在练最基础的语法。比如查询Student表中所有学生的信息查询成绩表中所有记录并按照分数降序排列查询姓“张”的老师的个数等等。这类题表面简单但包含了很多容易被忽略的细节。举个例子查询姓“张”的老师的个数许多人直接SELECT COUNT() FROM Teacher WHERE Tname LIKE 张%就算完事。但如果表里有重名教师或者后续业务中允许教师姓名为空这个结果就不严谨。更稳的写法是先确认Tname字段是否有唯一约束再决定用COUNT()还是COUNT(DISTINCT Tname)。刷基础题的时候多问自己一层“为什么这么写、有没有边界情况”比单纯把结果跑出来要有用得多。2.2 聚合与分组GROUP BY、HAVING、聚合函数的关系中间段的题目里有一大批是围绕聚合查询展开的查询每门课程的平均分、查询每名学生的选课门数、查询平均成绩大于60分的学生、查询选修了至少两门课程的学生学号。写这类题时最容易犯的毛病是把WHERE和HAVING混用。这两者的区别特别实在WHERE是在分组之前过滤原始行HAVING是在分组之后过滤聚合结果。举个典型场景想查平均分大于60的学生你只能在分组后用HAVING AVG(score) 60来判断因为AVG是针对每个分组计算的在分组前根本不存在。反过来如果只是想过滤掉成绩为NULL的记录那就要放在WHERE里先处理否则聚合结果会带上NULL的干扰。这类题真正考验的不是语法记忆而是你脑中有没有“分组思维”你要按什么维度看数据、聚合完之后还剩下什么信息、哪些字段可以在SELECT中直接出现哪些必须放进聚合函数——这些判断才是SQL水平的真正分水岭。2.3 多表连接JOIN系列与关联条件50题里有一半以上需要多表关联。最基本的是查询所有学生的学号、姓名、选课数、总成绩这需要Student表和SC表做左连接再复杂一点查询每门课程被选修的学生人数需要Course和SC做关联后按课程分组。连接题的难点从来不是记住LEFT JOIN、RIGHT JOIN、INNER JOIN的语法而是搞清楚业务到底需要哪张表的全量数据。一个我反复强调的检查方法先从业务描述中找出“主表”也就是结果中必须要保留全部记录的哪张表再找出“从表”也就是用来补充附加信息的表。比如“查询所有学生的选课情况没选课的学生也要显示”这句话里“所有学生”就是主表必须用LEFT JOIN把Student放在左侧。很多人一上来就写INNER JOIN结果没选课的学生被静默过滤掉这就是典型的业务理解不到位。2.4 子查询与IN/EXISTS的选择这套题里还有几道必须用子查询才能优雅解决的查询没学过“张三”老师课的同学、查询和“01”号同学所学课程完全相同的同学。这类题目如果用连接要么写出巨长的中间结果要么逻辑绕来绕去而换成子查询会清晰很多。这里我想多说一句IN和EXISTS的选择。很多老教材会告诉你“EXISTS比IN快”但这句话在新版数据库上已经不完全成立。现代优化器会把简单的IN子查询改写为半连接执行计划和EXISTS几乎没有差别。真正需要关注的是子查询结果集的大小如果子查询返回的结果集很小IN写起来更直观如果子查询涉及大表且外部表也很大EXISTS通常能提前返回减少不必要的扫描。练习题里两种写法都可以试一遍重点不是背结论而是学会用EXPLAIN看执行计划自己判断。2.5 窗口函数与行转列拉开差距的最后十题最后十道题基本就是面试中用来区分“会写SQL”和“写得漂亮”的题目。比如查询各科成绩前三名用传统写法得靠自连接加计数既绕又容易出错而用窗口函数DENSE_RANK() OVER(PARTITION BY CId ORDER BY score DESC)一行就能把排名算出来代码量直接少三分之一。行转列也是同样的道理。把每门课的成绩从行变成列传统做法是CASE WHEN加聚合函数写起来很繁琐但好懂用PIVOTSQL Server或GROUP_CONCAT配合条件聚合MySQL则更简洁。刷到这一层时我建议你不要只看一种写法而是把两三种方案都写一遍然后对比它们的执行效率、可读性和数据库兼容性。这种对比训练恰恰是实际项目中做SQL评审时最需要的能力。3. 五道经典题目拆解从读题到写SQL的完整思路3.1 题目查询“01”课程比“02”课程成绩高的学生这题是一道经典的自连接入门题。很多人第一反应是分两次查询分别查出两门课的成绩再在代码里做比较。这当然能跑但用SQL解决会更优雅——把SC表和自己连接起来一条语句搞定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;这条SQL的要点在于给同一张SC表起了两个别名逻辑上把一张表当两张表来用。a表示“01”课程的成绩记录b表示“02”课程的成绩记录通过SId把它们关联到同一个学生身上。很多新手第一次看到自连接会晕我的建议是在草稿纸上把同一张表画成两份一份标a、一份标b再一条条连线思路立刻就清楚了。这个习惯我一直保留到现在处理复杂的关联查询时非常管用。3.2 题目查询平均成绩大于60分的学生学号和平均成绩这题是HAVING的经典考场。错误写法很常见先用WHERE把score过滤一遍再分组或者干脆把WHERE AVG(score) 60这种语法错误写上去。正确做法很简洁SELECT SId, AVG(score) AS avg_score FROM SC GROUP BY SId HAVING AVG(score) 60;我想重点解释一下为什么AVG放在HAVING里因为平均成绩是分组之后才计算出来的值在分组前的行级别数据中根本不存在。这道题的价值在于帮人建立“SQL执行顺序”的意识——数据库实际执行时先FROM找到表再WHERE过滤行再GROUP BY分组之后才轮到HAVING过滤分组最后才是SELECT取出结果。只要把这个顺序刻在脑子里再去判断WHERE和HAVING该放哪里基本就不会错了。3.3 题目查询各科成绩前三名各科前三名是窗口函数最经典的练手场景。没有窗口函数时你得这样写对每一门课找出该课程中比当前分数高的学生个数如果少于3个说明当前学生排在前三。这个写法可用但性能不好代码也不直观SELECT s.SId, s.CId, s.score FROM SC s WHERE ( SELECT COUNT(*) FROM SC t WHERE t.CId s.CId AND t.score s.score ) 3 ORDER BY s.CId, s.score DESC;窗口函数解法就清爽很多SELECT SId, CId, score FROM ( SELECT SId, CId, score, DENSE_RANK() OVER (PARTITION BY CId ORDER BY score DESC) AS rk FROM SC ) ranked WHERE rk 3;这里我特意选了DENSE_RANK而不是ROW_NUMBER因为DENSE_RANK会为并列分数保留同一名次。比如一门课有两个人并列第一DENSE_RANK下第二名就是第三名而ROW_NUMBER会硬分出一个第一和一个第二。业务上“前三名包含并列”和“前三名不包含并列”是两种完全不同的需求练习时把RANK、DENSE_RANK、ROW_NUMBER三者的差异全部跑一遍比背定义要深刻得多。3.4 题目查询没学过“张三”老师课的同学这题看起来绕其实是Not In/Not Exists的典型应用。解题思路可以先拆成两步第一步查出张三老师教过哪些课程第二步找出没有选过这些课程的学生。基于这个思路直接用NOT EXISTS写最清晰SELECT SId, Sname FROM Student WHERE NOT EXISTS ( SELECT 1 FROM SC JOIN Course ON SC.CId Course.CId JOIN Teacher ON Course.TId Teacher.TId WHERE Teacher.Tname 张三 AND SC.SId Student.SId );很多初学者会本能地用NOT IN大概写成WHERE SId NOT IN (SELECT ...)。在这个场景下两者结果一样但我想提醒一点如果子查询结果中出现了NULLNOT IN会直接返回空结果因为“不在这个集合里”遇到NULL时会变成未知而不是真值。这个坑非常隐蔽遇到线上数据出现诡异空结果时第一个怀疑对象就应该是它。出于稳妥我一般优先使用NOT EXISTS这也是一线开发者踩过无数坑后的共识。3.5 题目行转列显示每门课的成绩列最后一类经典题是把成绩表的行转成列比如把语文、数学、英语三科成绩从多行变成一行三列。这里我用MySQL或SQL Server通用的写法CASE WHEN加聚合函数SELECT SId, MAX(CASE WHEN CId 01 THEN score END) AS 语文, MAX(CASE WHEN CId 02 THEN score END) AS 数学, MAX(CASE WHEN CId 03 THEN score END) AS 英语 FROM SC GROUP BY SId;为什么要包一层MAX因为GROUP BY SId之后每个学生分组里会有多行成绩记录而CASE WHEN只把符合条件的行置为分数、其他行置为NULL如果不做聚合数据显示出来仍然是多行。MAX在这里的作用是去掉NULL、保留非空值碰到非数值场景也可以换成MIN。这一步理解透了遇到行转列需求你就不会再头皮发麻。如果用的是SQL Server可以直接写PIVOT代码更简洁但PIVOT的列需要提前写死动态列时反而不如CASE WHEN灵活。练习题阶段我建议两种都试试体会各自的边界。4. 刷题踩坑实录去重、连接、慢查询与安全隐患4.1 DISTINCT和GROUP BY去重到底选哪个去重查询是热搜里的高频词也是刷题时容易炸的第一个坑。SELECT DISTINCT和GROUP BY都能去重但两者语义有差异DISTINCT是对SELECT出来的整行去重GROUP BY是按指定列分组分组后还能配合聚合函数做统计。我给你一个比较务实的选型建议如果只是简单去重不需要任何聚合用DISTINCT可读性更好如果去重的同时还要统计数量、平均值等那只能走GROUP BY。另外提醒一点在MySQL里GROUP BY的隐式排序行为在8.0之后已经被移除但在某些老版本上GROUP BY可能会附带排序影响性能。所以刷题时不要只看结果对不对多留心版本差异这也是贴近实战很重要的一项训练。4.2 JOIN条件写错导致的笛卡尔积爆炸练习题因为数据量小就算JOIN条件写错笛卡尔积也就几十行肉眼不太容易发现。但同样的错误放到生产环境动辄就是上亿行的中间结果直接可以把数据库拖垮。我让新人做50题时会专门让他们检查一个细节写完JOIN之后先预估一下结果集的行数应该是多少再和实际返回的行数对比。比如SC表有100条成绩记录但如果你在JOIN Student时忘了写关联条件结果可能变成几万行。这种“先估后查”的习惯一旦养成线上出问题时的排查速度会快很多。另一个建议是ON条件里只写关联关系过滤条件最好放到WHERE里这样语义更清晰别人review你的代码时也不会产生误解。4.3 慢SQL优化EXPLAIN主要看哪些信息题目刷到后半段自然要考虑性能问题。搜索热词里“慢sql优化 explain主要看哪些信息”上了榜说明这是绝大多数开发者都绕不过去的问题。EXPLAIN是MySQL分析执行计划的核心工具但很多新手一打开EXPLAIN结果就懵不知道先看哪列。我的经验是优先看四列type、key、rows、Extra。type列显示访问类型从好到差大致是const、eq_ref、ref、range、index、ALL。如果看到ALL说明是全表扫描基本可以判断这个查询会慢。key列显示实际用到的索引如果为NULL说明索引没生效。rows列是优化器预估要扫描的行数这个数字越大查询越重。Extra列里如果出现Using filesort或者Using temporary说明出现了额外的排序或临时表通常需要优化。举个例子50题中用子查询查“没学过张三老师课的同学”如果Student表足够大用NOT EXISTS时EXPLAIN的type通常能走索引而用NOT IN有时候会退化成全表扫描。这就是为什么我一直强调练习时不要只求结果对多跑几次EXPLAIN你会慢慢建立起对SQL开销的直觉。4.4 明显会踩的安全坑拼接SQL与SQL注入练习题本身不涉及安全但热搜词里“sql注入万能密码绕过”热度这么高说明还是有不少人会在实际项目里踩到拼接SQL的坑。刷题时你写的都是常量条件比如WHERE SId 01但到了业务代码里如果直接把用户输入拼进SQL就很容易被注入。安全的习惯其实是“提前种下”的写SQL时就要有参数化的概念不要养成字符串拼接的习惯。无论是JDBC的PreparedStatement还是其他编程语言中的参数化查询都应该把用户输入当作参数传进去而不是拼到SQL文本里。刷题阶段虽然不需要写业务代码但每次写WHERE条件时都主动想一想“这个条件将来如果由用户输入控制会不会有风险”这种意识会跟着你进到真实项目里帮你避开很多灾难性的问题。5. 刷完题之后面试应答和实战迁移5.1 从刷题到面试除了给答案还要讲清思路50题全部刷完只是第一步面试时能不能把你脑子里的思路表达出来是另一项重要能力。我模拟面试时发现很多候选人能写出正确SQL但被问到“为什么这里用LEFT JOIN而不是INNER JOIN”就卡住了。这说明他们知道怎么写却不清楚背后的判断逻辑。我的建议是每做完一道题强制自己用三句话复述解题思路第一句这道题需要哪几张表的数据第二句表之间通过什么字段关联主表是哪张第三句是否需要分组或窗口函数最终结果的粒度是什么。能把这三句话讲清楚面试官对你的评价会比只会闷头写SQL的候选人高一个档次。5.2 把四表模型迁移到真实业务场景很多人刷完题有个困惑练习题都做明白了为什么一回到公司的订单表、用户表、日志表上还是不会写原因是真实业务表动辄几十上百个字段而且数据质量参差不齐有重复、有空值、有脏数据。这和练习题里干干净净的四张表完全不同。我的做法是把50题里的思路抽象成通用的分析框架需要多表关联时先想清楚主表和粒度需要聚合统计时先确定维度和度量需要去重时先明确业务上“重复”的定义到底是什么。这套框架一旦建立不管表结构多复杂你都能像套公式一样把问题拆解清楚。带新人的时候我也鼓励他们拿公司的真实业务写查询但先在测试库上验证避免误触生产数据。5.3 后续进阶方向建议刷完50题之后如果你还想继续深入我建议按这三个方向走第一系统学习窗口函数的高级用法包括滑动窗口、帧范围的设置这是做漏斗分析、留存分析等场景的利器第二练习存储过程或SQL调优把同一道题用不同写法实现再用执行计划和实际耗时对比优劣第三选择一个数据库产品深入下去比如SQL Server或MySQL读读官方文档搞清楚索引结构、锁机制、事务隔离级别这些底层概念。这三个方向正好对应了数据分析、后端开发和数据库管理三条职业路径你可以根据自己的规划去选择。但不管选哪个这套50题打下的底子都不会白费——它们永远是你在需要的时候可以随时调用的肌肉记忆。最后再分享一点个人体会刷题这件事真不在多而在于每一道题你都搞得足够透。我自己已经把这套题用了很多年每次重新做一遍还是能发现新的写法或新的理解角度。建议你也别急着把50道题一口气刷完一天做五道、每道题把不同解法都跑一遍比一天刷二十道、只求“对答案”要扎实得多。希望这套题能成为你SQL内功修炼路上的一块好磨刀石。