首页 / 资讯中心 / 文章详情

MySQL SQL练习题高频题型解析:从执行顺序到窗口函数实战

MySQL SQL练习题高频题型解析:从执行顺序到窗口函数实战 ★ FEATURED ARTICLE
面试别人的时候我见过太多简历写着“精通SQL”的人拿到一张只有十几行数据的题目纸就开始手心冒汗。写出来的查询要么跑得特别慢要么结果对不上最要命的是连“为什么错”都说不清。MySQL的SQL练习题表面看考的是语法实际上考的是三件事对表结构的理解、对SQL执行顺序的把握、以及对边界条件的敏感度。这篇文章不打算给你一堆题目和答案让死记硬背而是挑几类最高频、最容易翻车的题型从建表、造数开始一步步拆解背后的判断逻辑。适合准备笔试面试的同学也适合刚把增删改查学完、想系统巩固MySQL的朋友。1. 练习环境与建表先把题目落到能跑的SQL上很多人在线刷题时有个坏习惯题目看两眼就急着写SELECT根本不管表结构长什么样。说实话大部分SQL练习题的“难点”恰恰藏在表结构和隐藏条件里——“成绩第二高”可能并列好几个人“连续登录3天”需要先处理重复打卡记录“没有参加考试”意味着NOT IN可能踩到NULL坑。这些在纸上画不清楚动手写SELECT一定错。所以我的建议一直是别到处零散刷题自己建一套固定表结构的练习库反复演练几类套路远比刷一百道不重样的题有效得多。1.1 一套标准表结构学生、课程、教师、成绩下面这套表是我个人推荐给新人的“练手标准”它模拟了一个最简单的选课系统能覆盖绝大多数面试题和期末笔试题CREATE TABLE student ( sid INT PRIMARY KEY, sname VARCHAR(50) NOT NULL, gender CHAR(2), class_id INT, birthday DATE ); CREATE TABLE teacher ( tid INT PRIMARY KEY, tname VARCHAR(50) ); CREATE TABLE course ( cid INT PRIMARY KEY, cname VARCHAR(50), teacher_id INT ); CREATE TABLE score ( sid INT, cid INT, score DECIMAL(5,2), PRIMARY KEY (sid, cid) );把score表的主键设计成(sid, cid)联合主键是从结构上保证同一个学生同一门课只有一条成绩记录。很多练习题的答案里习惯性写DISTINCT但真实项目中遇到这种“明显不该重复”的数据第一反应应该是去查数据哪里脏了而不是用DISTINCT硬抹掉。这是我在实际业务里踩过的坑。接着插入几行覆盖题型的数据你可以自行扩充到几百行INSERT INTO student VALUES (1, 张三, 男, 101, 2003-05-12), (2, 李四, 女, 101, 2003-08-20), (3, 王五, 男, 102, 2002-11-02), (4, 赵六, 女, 102, 2003-01-15); INSERT INTO teacher VALUES (1, 周老师), (2, 吴老师), (3, 郑老师); INSERT INTO course VALUES (101, 数据库原理, 1), (102, 数据结构, 2), (103, 操作系统, 3); INSERT INTO score VALUES (1, 101, 92), (1, 102, 85), (1, 103, 88), (2, 101, 85), (2, 102, 92), (2, 103, 95), (3, 101, 70), (3, 102, 78), (4, 102, 88), (4, 103, 91);一个小建议练习时不要只满足于这一份小数据可以用存储过程往score表里灌几千行随机数据。数据量一大你才能体会索引缺失、排序文件、临时表这些问题是怎么真实发生的否则在十几行数据上任何写法看起来都是“对的”。1.2 拿到题目先做三件事再写代码第一件事是划清边界。题目里凡是有“每”“各”“连续”“第N”“没/从不”这类词必须先确认分组维度和过滤条件边界划错后面全白写。第二件事是定执行顺序。SELECT的执行逻辑不是从SELECT开始的而是先FROM、再WHERE、再GROUP BY、再HAVING、再SELECT、最后ORDER BY和LIMIT。很多题写错就是因为把WHERE和HAVING的位置放反了、把别名的使用范围想错了。第三件事是验结果。写完SQL别急着看标准答案先在脑中用那张小表手算一遍预期输出再把结果跑出来对比。如果对不上说明你对某个语法的理解有偏差这恰恰是练习最有价值的部分。这三件事听起来啰嗦但每次做题走一遍流程能帮你避开大部分低级失误。2. 基础查询题过滤、排序、去重里暗藏的坑基础题看上去简单但最容易被拉分。因为考点往往不是“你会不会写WHERE”而是“你知不知道这个语法在极端情况下会变成什么样”。2.1 成绩前两名为什么用子查询而不是LIMIT“查询每门课成绩前两名”这道题我见过至少20个版本考试爱考、面试也爱考。最常见的错误是先ORDER BY score DESC再LIMIT 2结果查出来是“整张表前两名”根本不是“每门课前两名”。先用不依赖窗口函数的写法理解它的判断逻辑SELECT s.cid, s.sid, s.score FROM score s WHERE ( SELECT COUNT(DISTINCT s2.score) FROM score s2 WHERE s2.cid s.cid AND s2.score s.score ) 2 ORDER BY s.cid, s.score DESC;这段逻辑是对每一行成绩统计同一门课里“比它高或跟它一样高的成绩有几种”如果不超过2种它就属于这门课的前两名。用COUNT(DISTINCT)而不是COUNT(*)是为了把并列分算成同一个名次。比如第一名92分、第二名85分那么85分的学生也会被算进来因为“比它高或相等的成绩”只有两种。这道题同时也是窗口函数最经典的引入场景后面我会专门讲ROW_NUMBER、RANK、DENSE_RANK三兄弟的区别。这里先记住一个结论如果题目没有任何“并列名次”的特殊要求直接用窗口函数如果题目明确说“可以有并列”优先用DENSE_RANK。2.2 没参加某门考试的学生NOT IN踩到NULL有多痛“查询没有考过数据库原理cid101的学生”是IN、NOT IN、EXISTS、NOT EXISTS的经典战场。先看一个看起来很正常的错误写法SELECT * FROM student WHERE sid NOT IN (SELECT sid FROM score WHERE cid 101);如果score表里存在一条sid为NULL的脏数据这个查询会返回空集合。原因在于NOT IN的底层逻辑是“不等于子查询返回的任意一个值并且不等于NULL”而“不等于NULL”这个判断永远不成立所以整个条件变成假结果空空如也。换用NOT EXISTS就安全得多SELECT * FROM student s WHERE NOT EXISTS ( SELECT 1 FROM score sc WHERE sc.sid s.sid AND sc.cid 101 );NOT EXISTS是逐行关联判断只要子查询里找不到“这个学生的101课程记录”就保留这个学生完全不关心NULL值的事。这个知识点几乎必考而且很容易翻车。我在实际开发里也见过不少同事因为NOT IN踩到NULL导致数据对不上查了半天才找到根因。2.3 多字段排序与DISTINCT组合题基础题里还有一种高频考法先按班级排序再按成绩降序。这种题只要注意一个点ORDER BY后面的字段顺序代表优先级SELECT st.class_id, st.sname, sc.score FROM student st JOIN score sc ON st.sid sc.sid ORDER BY st.class_id ASC, sc.score DESC;这里有个不值得踩的坑如果先写了一个字段方向是DESC后面又有个字段方向还是DESC或者两个字段方向相反很容易写成ORDER BY class_id DESC, score ASC。做题时每个字段后面都显式写上ASC或DESC看起来多打几个字但能避免方向写错。至于去重DISTINCT只能作用于SELECT子句里的所有列组合比如SELECT DISTINCT class_id, gender它去重的是(class_id, gender)这个组合不是单独去重class_id。遇到“查询所有班级编号”这种题要么SELECT DISTINCT class_id要么GROUP BY class_id。两者结果可能一样但语义完全不同后面GROUP BY部分我会详细展开。3. 聚合分组题GROUP BY、HAVING和ONLY_FULL_GROUP_BY聚合题是SQL练习题里的大头。原因很简单它最能体现一个人是不是真懂“行”和“组”的区别。很多人在非聚合列上翻车就是没搞清楚这一点。3.1 每班平均分与“平均分80的课程”先说最典型的“统计每个班级的平均分按平均分降序”。看到“每个”基本可以确定要GROUP BY。但班级信息在student表成绩在score表所以得先JOIN再分组SELECT st.class_id, ROUND(AVG(sc.score), 2) AS avg_score FROM student st JOIN score sc ON st.sid sc.sid GROUP BY st.class_id ORDER BY avg_score DESC;再升级一点“查询平均分大于80分的课程”其实不用JOIN单表score就能做SELECT cid, AVG(score) AS avg_score FROM score GROUP BY cid HAVING AVG(score) 80;注意这里把过滤条件放在HAVING因为平均分是分组之后才计算出来的结果WHERE压根不知道AVG是什么。3.2 WHERE与HAVING的分工很多人会混WHERE和HAVING的分工一句话就能说清WHERE是在分组之前过滤原始行HAVING是在分组之后过滤聚合结果。举一道常见扩展题“查询平均分大于80分的班级且只统计男生成绩。”SELECT st.class_id FROM student st JOIN score sc ON st.sid sc.sid WHERE st.gender 男 GROUP BY st.class_id HAVING AVG(sc.score) 80;性别条件放在WHERE里先过滤掉女生成绩剩下的行再分组求平均。如果把gender男放进HAVINGMySQL会报错或者行为不确定因为HAVING后面一般是聚合条件而不是原始行字段条件。我见过很多人写聚合题时把条件一股脑全塞进HAVING结果数据量一大查询就特别慢。HAVING的出现往往意味着MySQL要先算完整组再做过滤能提前用WHERE干掉的行绝不要留到分组之后。3.3 ONLY_FULL_GROUP_BY为什么SELECT里不能随便放列MySQL 5.7及以后的默认sql_mode里包含ONLY_FULL_GROUP_BY它强制要求SELECT中出现的非聚合列必须出现在GROUP BY中。这其实是好事因为它能从语法上杜绝歧义。看这个错误写法-- 开启了ONLY_FULL_GROUP_BY时会直接报错 SELECT st.class_id, st.sname, AVG(sc.score) FROM student st JOIN score sc ON st.sid sc.sid GROUP BY st.class_id;为什么报错因为一个班级里可能有多个学生比如101班有张三和李四你让数据库显示sname它不知道该显示哪个人。除非你明确告诉它“随便拿一个”否则MySQL拒绝执行。用ANY_VALUE(sname)可以绕过报错但它返回的往往是随机结果在业务上没有意义。遇到这种题第一反应应该是改分组粒度把sname加进GROUP BY或者改成子查询、窗口函数。而不是想办法绕过校验。4. 连接查询题INNER JOIN、LEFT JOIN、自连接怎么选连接查询是笔试里的“分水岭”。简单题靠WHERE关联就能混过去但一旦涉及NULL语义、驱动表选择、自连接很多人就乱了。4.1 所有学生及其选课情况LEFT JOIN还是INNER JOIN“查询所有学生及其选课情况包括没选课的学生”关键词是“所有”和“包括没选课”。主表是student关联表是score缺数据的一定是score所以用LEFT JOINSELECT st.sid, st.sname, sc.cid, sc.score FROM student st LEFT JOIN score sc ON st.sid sc.sid ORDER BY st.sid;如果改成“查询至少选了一门课的学生及成绩”没选课的不需要出现那就是INNER JOIN。判断标准很简单主表是“所有”的那个表关联表是“可能缺数据”的表。主表写在前从表LEFT JOIN上去结果里主表多出来的行会用NULL补齐关联字段。有个细节值得注意LEFT JOIN出来的NULL行如果用WHERE去过滤一不小心就会把主表行过滤掉。比如想在查所有学生选课情况时“只看有成绩的”你直接在WHERE里写sc.score IS NOT NULL那LEFT JOIN就名存实亡因为NULL行全被删了。4.2 自连接题“查询和‘张三’在同一个班级的学生”自连接是很多人第一次接触时觉得抽象的东西因为它本质上是把一张表当成两张表来用。一个别名当成“我”一个别名当成“别人”SELECT others.sid, others.sname FROM student me JOIN student others ON me.class_id others.class_id WHERE me.sname 张三 AND others.sid me.sid;核心点在于JOIN条件用class_id相等过滤条件用sname张三最后再用others.sid me.sid把自己排除掉。第二个条件非常容易忘忘了就会把自己也算进结果笔试里这属于“细节分”的典型失分点。自连接不止这一种考法比如“查询比同班同学平均成绩高的学生”“查询相同分数出现的所有组合”都是同一个套路一张表起两个别名分别扮演两个角色。4.3 N张表JOIN最少要有N-1个关联条件这是一个可以救命的自查规则N张表JOIN在一起起码要有N-1个关联条件否则就会产生笛卡尔积。比如三张表JOIN通常至少有2个ON条件不然每张表的每一行都会跟另外两张表的每一行无序组合查询慢到让你怀疑人生。千万不要把WHERE里的等值条件当成ON条件来凑数。ON和WHERE虽然都写着等值关系但执行时机不同。外连接中ON条件在连接时判断WHERE条件在连接完成后过滤同样的写放在不同位置结果可能完全不同。做题时一定要想清楚哪些关联条件属于表关系本身哪些属于业务过滤条件。5. 窗口函数题分组排名、连续区间与累计值窗口函数是近年来SQL练习题和面试题的重头戏把窗口函数练好很多问题都能大幅简化。它的核心价值在于“不压缩行数”——GROUP BY会把多行压缩成一行丢掉原始行细节窗口函数则在保留每一行的同时在旁边算出一个汇总信息。这个特性能解决大量“既要明细又要排名”的需求。5.1 每门课前两名有了窗口函数就是降维打击之前用相关子查询费劲写的“每门课前两名”用窗口函数可以写成这样WITH t AS ( SELECT cid, sid, score, DENSE_RANK() OVER (PARTITION BY cid ORDER BY score DESC) AS rk FROM score ) SELECT cid, sid, score FROM t WHERE rk 2 ORDER BY cid, score DESC;PARTITION BY cid相当于给每门课单独划了一组ORDER BY score DESC决定组内的排名方向DENSE_RANK给每行一个密集排名。密集排名的意思是并列第一的两行都拿到1下一名拿2不会因为并列而跳号。这里把三个排名函数说清楚函数并列时行为典型使用场景ROW_NUMBER并列也强行编顺序每人不同号确定唯一序号、分页RANK并列同号后续名次会跳过体育比赛排名DENSE_RANK并列同号后续名次不跳过成绩分级、取TopN考试里如果只问“前两名没说明并列”用ROW_NUMBER一般不会错如果题目明确提到“成绩并列的都算”用DENSE_RANK。5.2 连续登录3天的用户日期与排名的差值用法这道题在真实业务里非常常见也是近两年SQL练习里的网红题。假设有一张登录流水表CREATE TABLE login_log ( user_id INT, login_date DATE );表中数据形如用户1在1月1日、1月2日、1月3日登录用户2在1月1日、1月3日、1月4日登录。要求找出连续登录至少3天的用户。直接判断“连续”很难但利用一个精妙思路可以把它变成分组问题如果某个用户连续登录那么“登录日期减去它在序列里的排名”是一个固定值。比如连续三天的日期是2、3、4排名是1、2、3相减分别是1、1、1自动归为同一组。WITH distinct_log AS ( SELECT DISTINCT user_id, login_date FROM login_log ), t AS ( SELECT user_id, login_date, DENSE_RANK() OVER (PARTITION BY user_id ORDER BY login_date) AS rk FROM distinct_log ) SELECT user_id FROM t GROUP BY user_id, DATE_SUB(login_date, INTERVAL rk DAY) HAVING COUNT(*) 3;为什么先用DISTINCT去重因为同一天如果有多次登录记录不去重会直接算成两天。为什么用DENSE_RANK而不是ROW_NUMBER如果登录日期有重复但没被完全去重或用ROW_NUMBER日期与排名的差值会受重复行影响导致分组错误。这题的优雅之处在于把一个时间序列问题转化成了普通的GROUP BY和COUNT。下次再碰到“连续签到N天”“连续下单N周”都可以沿这条思路套。5.3 SUM OVER累计值神器“统计每个用户累计消费金额”是另一个窗口函数高频考题。订单表orders(user_id, order_date, amount)要得到每次消费时的累计值SELECT user_id, order_date, amount, SUM(amount) OVER (PARTITION BY user_id ORDER BY order_date) AS cum_amount FROM orders;注意这里的ORDER BY不是管全局排序而是决定窗口内累计的方向。按日期从小到大累加就是累计金额如果漏掉ORDER BY窗口函数会把整组数据一次性汇总那一列就会变成总额而不是累计值结果完全不对。同样的写法还可以做移动平均、占比计算、环比同比。窗口函数一旦熟练很多原来要靠衣区域子查询来回折腾的场景都会变得简单。我在项目里用SUM OVER写过一个设备故障率区间统计原来跑十几秒的SQL优化到几百毫秒。6. 实操中容易翻车的隐藏考点练习题写多了你会发现数据库面试和工作中真正的坑往往不是复杂写法不会而是基础概念在特殊场景下的表现。6.1 排序规则与字符集中文排序可能不是你想象的那样“按姓名排序”这种题看起来太基础了但中文排序在不同规则下会得到不同结果。MySQL在utf8mb4_general_ci和utf8mb4_unicode_ci下的默认排序并不总一致而排序规则的选择又取决于建表时的DEFAULT CHARSET和COLLATE。如果业务要求按拼音首字母排直接ORDER BY sname多半不可靠。常见做法是把字段转成GBK编码后再排序SELECT sname FROM student ORDER BY CONVERT(sname USING gbk);这道题既能考察字符集知识又能考察实际开发中对多语言排序的处理意识。练习的时候别觉得它偏真遇到很多老系统里乱码般的中文排序问题你会感谢当年刷过这道题。6.2 查询去重和物理删除重复数据是两码事“SQL语句去重”这个热词背后其实藏着两种完全不同的需求。一种是查询结果里去重用DISTINCT或GROUP BY。比如“查询所有班级编号”用SELECT DISTINCT class_id就行。另一种是物理删除表中的重复行。比如表里因为程序跑批重复插入了多条相同记录只保留id最小的那一条。MySQL写起来很直观DELETE t1 FROM your_table t1 JOIN your_table t2 ON t1.dedup_key t2.dedup_key AND t1.id t2.id;它利用自连接找到所有“有同key且id比我大”的行并删除。这种题在面试里往往以“如何清洗一张脏表”的形式出现。如果同一个key有多行并且id是自增主键保留最小id即可。注意先备份别在生产环境直接跑DELETE。6.3 慢SQL优化答案对了不代表程序没问题练习题阶段很多人只看“结果是否正确”忽略了执行效率。但实际工作时一条SQL跑30秒和跑0.3秒的差距就是线上事故和日常作业的区别。判断SQL是否高效第一件事是EXPLAINEXPLAIN SELECT st.class_id, AVG(sc.score) FROM student st JOIN score sc ON st.sid sc.sid WHERE st.gender 男 GROUP BY st.class_id;重点关注EXPLAIN里的type列ALL代表全表扫描这是最需要警惕的ref和eq_ref代表通过索引定位表现较好。Extra列如果出现Using filesort或Using temporary通常意味着排序或分组没有走索引数据量大时就会暴露出性能问题。优化思路一般三步先给WHERE条件里的字段建索引再给JOIN的关联字段建索引最后看能不能用覆盖索引避免回表。这已经是慢SQL优化的老套路了但很多练习题爱好者压根没想过等到真实项目里被DBA点名批评才反应过来。7. 我的个人刷题习惯与建议聊到最后说一点我自己刷SQL练习题的切身体会。我见过很多人一上来就背题把每道题的答案背得滚瓜烂熟换个条件就懵了。SQL这个东西语法只是外壳真正值钱的是建模能力和对数据流动的理解。我建议刷题的时候准备一个错题本不是抄题目抄答案而是记录“当时为什么写错”是没看清分组维度是NULL语义问题还是对执行顺序理解偏差把这些错误分类之后你会发现自己的薄弱点特别集中。另一个行之有效的办法是举一反三。每做完一道题主动改条件再写一遍。比如刚写了“统计每个班级的平均分”就改成“统计每个班级男生的最高分和最低分”再改成“统计每门课不同分数段的分布”。同样的表结构通过变换条件把GROUP BY、窗口函数、CASE WHEN串起来比去找下一道新题收获大多了。最后练习量到一定程度后尽量脱离在线平台直接在本地MySQL命令行或自己搭的开发环境里跑。能亲手操作建表、插入数据、修改数据、看EXPLAIN执行计划这些是刷题网站无法替代的经验。SQL的学习没有捷径但方向对了、方法对了进步速度会让你自己都感到意外。
阅读完成 · 觉得有帮助?
咨询建站