说句实在话,光看MySQL教程和视频,不如认认真真做一套“mysql练习题(基础-进阶)”进步快。很多初学者去面试,连自连接、窗口函数、事务隔离级别的区别都说不清,但问起来又说自己看过书;真正动手刷题以后,才发现知识缺口原来这么大。这篇文章不是扔给你几十道题就完事,而是把题背后的知识点拆开,告诉你每一类题在练什么、怎么练、会遇到什么坑。适合正在学MySQL的在校学生、用JavaWeb或Spring Boot搭项目的开发者,以及准备数据库岗位面试的求职者。
1. 练习题的整体设计思路:先别急着写代码,把知识地图铺开
1.1 核心知识点拆解:基础到进阶到底差在哪
MySQL这个领域看着简单,实际上知识点铺开是非常散的。很多人目标不清晰,一上来就刷存储过程,结果连LEFT JOIN都写不顺,最后卡死在报错里。我建议先按一条主线来分:数据定义、数据操作、查询分析、设计建模、性能调优、编程对象。基础阶段重点做数据定义、数据操作、查询分析,比如CREATE TABLE、INSERT、UPDATE、DELETE、SELECT、WHERE、ORDER BY、GROUP BY、多表连接、常用函数。进阶阶段再做索引优化、事务隔离级别、锁机制、存储过程、触发器、视图、窗口函数。这样正好覆盖热搜里“mysql排序”“mysql update语法”“mysql常用函数”这些基础词,也能碰到“mysql存储过程”“mysql锁表”“mysql触发器”这些进阶词。
这里有一个很关键的认识:基础题不是简单题,进阶题也不是炫技题。基础题练的是写正确SQL的能力,进阶题练的是写出高效且健壮SQL的能力。比如同样是查学生成绩,基础题要求你把结果查出来,进阶题要求你用EXPLAIN分析本次查询走了哪个索引,有没有Using filesort,能不能用覆盖索引优化。这两种题的思考深度完全不同,但知识体系是接在一起的。
1.2 为什么练习比只看教程更有效
看教程是输入,做题是强制输出,能暴露出“我以为我会了”的假象。MySQL这门技术尤其讲究动手,因为同样的SQL,在不同的数据量、不同的索引条件下,执行计划可能完全不一样。我见过很多人把“SELECT * FROM student WHERE name LIKE '张%'”写得很溜,但问他们为什么在name字段上建了索引,前缀模糊查询却不一定走索引,就开始支支吾吾。这种问题不是靠背就能解决的,必须实际跑一遍EXPLAIN才知道结果。
我做练习题的习惯是先手写SQL,再放到Navicat、DBeaver或者命令行里验证结果,最后用EXPLAIN看执行计划,三个步骤一步都不能省。如果只是在客户端里直接执行参考答案,那练的只是打字速度,不是数据库思维。真正有价值的练习是允许自己写错,然后去看错误提示,再倒推原因。比如报错“Unknown column”的时候,是字段名拼错了还是表别名没生效?报错“Duplicate entry”的时候,是哪条唯一约束被撞上了?这些问题只有踩过一次才会刻进脑子里。
1.3 选题原则与难度曲线
练习题要遵循小步快跑的原则,不要一上来就给一套大综合。先单表,再多表,再子查询,再函数,再流程控制,再性能优化。每道题只解决一个核心点,但要有变式。比如练排序,就分别用ORDER BY单字段、多字段、配合LIMIT分页、配合CASE WHEN做自定义排序;练分组,就分别做统计数量、求平均分、取每组TopN。这样一套下来,同一个知识点至少被从三个角度敲打过,记忆自然深。
难度曲线可以设计成:基础查询 -> 统计分组 -> 关联查询 -> 复杂子查询 -> 索引进阶 -> 存储过程/触发器 -> 并发场景。这个顺序有个明显好处,就是每一步都用得到前置知识。比如做复杂子查询时,你得先会写基础关联查询;做存储过程时,你得先熟悉函数、变量和流程控制。练习题如果东一榔头西一棒子,学完很容易忘,但按这个曲线走,最后做综合题时会有一种拼图拼完的感觉。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础阶段核心题型的实操要点
2.1 建库建表:多花五分钟,后面省五小时
很多同学拿到练习题就急着写SELECT,完全不看表结构是什么样的。我反而觉得建表是最值得练的,因为表结构设计会影响后续所有查询能不能写、写得好不好。练习题里起码要有三张表:学生表、课程表、成绩表。建表的时候要考虑数据类型选择,比如年龄用TINYINT,成绩用DECIMAL(5,2),主键用INT或BIGINT,创建时间用DATETIME。热搜词里的“mysql中int+5”很有意思,很多人不知道INT是定长整数,“INT + 5”只是一个算术表达式,并不会改变字段类型;如果想把某个整数记录加5,应该用UPDATE语句去改字段值,而不是在表设计上纠结。
字符集和排序规则也要提前统一。我推荐utf8mb4和utf8mb4_general_ci,避免中文乱码,同时兼容Emoji。如果表已经建好才发现乱码,后面迁移数据的成本远比现在重新建表高。练习题里故意让你把学生姓名、课程名称都设计成VARCHAR加COMMENT,目的就是养成写注释的习惯。真实项目里没有人愿意维护一张没有注释的表,字段代表什么意思全靠猜,那是灾难。
另外,主键自增和唯一约束最好在建表时直接声明。练习题经常要求“某个字段不能重复”,比如成绩表里一个学生同一门课只能有一条成绩记录,那就应该建立联合唯一索引UNIQUE KEY uk_stu_course (student_id, course_id)。这样当你用INSERT插入重复数据时,MySQL会直接报错,你就能看到Duplicate entry的原因。很多人去搜索“mysql设置唯一已经有重复数据库”,其实就是先有了重复数据再想加唯一索引,结果失败。刷题时一定要故意构造这种场景,体会一下先清重复数据再加约束的流程。
2.2 单表查询与排序:那些最基础也最容易被忽略的细节
单表查询是SQL的起点,但很多人从来没认真研究过排序。题目示例:查询所有学生的姓名和出生年份,按年龄从大到小排序。这题看似简单,考点其实是YEAR()函数和ORDER BY的组合,以及NULL值排序问题。MySQL默认NULL最小,所以排序时没有出生日期的学生会排在最前面,这往往和业务预期不符。正确做法是ORDER BY birth_date IS NULL, birth_date ASC,把空值放到最后。
还有一个高频问题来自热搜词“mysql的or能去重吗”,这个问题非常基础但很经典。答案是:OR本身不去重,只有DISTINCT或GROUP BY才有去重效果。练习时可以写:查询计算机系或者成绩大于90分的学生,分别用OR、IN、UNION三种写法,然后对比结果。OR虽然简单,但在某些场景下会破坏索引合并,导致全表扫描;用IN或UNION反而可能让优化器有更多发挥空间。这种题不跑一遍执行计划,光靠背结论是没有感觉的。
分页排序也是容易翻车的地方。如果ORDER BY后面只有一个非唯一字段,比如只按成绩排序,分数相同的行的顺序在两次查询中可能不同。分页到第二页时,就可能出现第一页已经看过的数据。所以分页查询建议在排序字段后面追加主键,比如ORDER BY score DESC, id ASC。这个习惯很多资深DBA都会用,练习题里值得专门写一道。
2.3 聚合与分组:GROUP BY的隐藏陷阱
分组统计是我见过出错率最高的练习题类型,几乎每轮面试都会有人在这里翻车。题目设计:统计每门课程的平均分、最高分、最低分、人数。核心考的是GROUP BY加聚合函数,看起来很容易,但陷阱非常多。很多新手会在SELECT后面写非聚合列,比如既查学生姓名又查COUNT(*)。在MySQL默认的sql_mode没有开启ONLY_FULL_GROUP_BY时,这样的SQL可能不报错,但查出来的学生姓名是随机值;如果开启,就会直接报错。所以我强烈建议练习环境把sql_mode配置成ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,逼自己写规范SQL。
另一个坑是HAVING和WHERE的区别。WHERE在分组前过滤,HAVING在分组后过滤,两者语义不同,性能差异也很大。可以设计一道题:统计平均成绩大于80分的课程,但要求只统计那些成绩不低于60分的记录。很多同学会写成WHERE score >= 60放前面,再HAVING AVG(score) > 80,这是对的;但也有同学把所有条件都塞进HAVING,结果不仅逻辑容易错,执行效率还特别低。做题时一定要用EXPLAIN看一眼,观察有没有Using temporary或者Using filesort,这能帮你理解为什么HAVING要少用。
分组后如果要取每组前N名,就到进阶范围了,但基础阶段可以先练一个简单版本:统计每个学生的选课门数和总学分,然后按总学分排序。这道题能同时练到JOIN、GROUP BY、SUM和ORDER BY,比单独练一个函数有意思得多。
2.4 UPDATE与DELETE:别忘了事务和LIMIT
热搜词里“mysql update语法”一直很靠前,但还是要说,很多练习题只练SELECT不练UPDATE和DELETE,这绝对是个误区。实际项目里数据变更才是风险最高的地方。我会设计一道题:把不及格的成绩增加5分,但是不能超过60分。这个题目必须用UPDATE加CASE WHEN,而很多人只会写SET score = score + 5,结果59分变成64分,逻辑就错了。正确写法是:
sql复制UPDATE score
SET score = CASE WHEN score + 5 > 60 THEN 60 ELSE score + 5 END
WHERE score < 50;
这里有几个细节值得注意。第一,WHERE条件必须明确,不写WHERE就是全表更新,在开发库上还能救,在生产库上那就是事故。第二,UPDATE之前先SELECT出来看影响行数,这个习惯比任何技巧都重要。DELETE也是同样的道理,最好先用SELECT确认要删的数据,再改成DELETE执行。第三,如果一次要更新大量数据,可以分批LIMIT更新,避免长事务锁表。
事务也是练习UPDATE的好场景。比如初始化数据时先START TRANSACTION,然后执行几条INSERT或UPDATE,再故意制造一个错误,最后ROLLBACK,看看数据有没有回滚。这个练习能帮你理解为什么实际业务中多条写操作要包在事务里。MySQL默认自动提交是开启的,如果没有显式事务,每条语句都是独立提交的,出错以后想回滚都来不及。
2.5 常用函数专项练习:字符串、日期、数字函数
MySQL常用函数是热搜词,也是基础题里非常容易出彩的部分。字符串函数要练SUBSTRING、CONCAT、LENGTH、CHAR_LENGTH、REPLACE、TRIM。日期函数要练YEAR、MONTH、DATE_FORMAT、DATEDIFF、DATE_ADD。数值函数要练ROUND、FLOOR、CEIL、RAND、ABS。聚合函数要练COUNT、SUM、AVG、MAX、MIN。练函数不能光看手册,要放到真实场景里用。
我常让学员做这样一组题:把学生姓名和性别拼接成一个字段,要求格式为“张三(男)”;查询本月过生日的学生;查询成绩表中存在几位小数位,并统一保留两位小数。这几道题会逼你去翻函数文档,也会让你意识到不同函数对NULL的处理逻辑不一样。比如CONCAT遇到NULL会返回NULL,但CONCAT_WS遇到NULL会跳过,很多人在拼接字符串时被这个细节坑过。用IFNULL或COALESCE处理空值是项目里的常规操作,练习题也要专门设计。
函数练习还涉及类型转换。比如热搜词里“mysql可以存储整数数值的是”这类问题,背后就是整数类型和字符串类型的区别。成绩如果是DECIMAL(5,2),你拿它和整数比较时MySQL会做隐式转换。隐式转换不一定报错,但会导致索引失效,这个问题要留到进阶性能优化里再讲。基础阶段只需要保证能用CAST和CONVERT显式转换,遇到隐式转换时能意识到。
3. 进阶阶段核心题型的方案拆解
3.1 多表关联:INNER JOIN、LEFT JOIN和子查询的取舍
多表查询是进阶的第一道坎。练习题必须覆盖三类:内连接、左连接,以及用EXISTS和NOT EXISTS表达子查询。比如题目:查询所有学生及其选修课程的成绩,没选课的学生也要显示。这必须用LEFT JOIN,如果用INNER JOIN,没选课的学生直接消失。另一个经典题目是:查询没有选任何课程的学生,可以用LEFT JOIN ... WHERE xxx IS NULL,也可以用NOT EXISTS。两种写法结果一样,执行计划却可能有差别,数据量大的时候尤其明显。
内连接和左连接的取舍,我用一个生活化类比:内连接像相亲,双方都对上才出现;左连接像追求者名单,左边的人不管有没有回应都会保留。实际项目里经常需要在统计报表中保留主表的全部行,比如查所有客户每个月的订单量,即使某月没有订单也要显示0。这时候用LEFT JOIN加IFNULL,比分组后再补数据方便得多。
子查询也不是越少越好。有些关联子查询写法很自然,但执行时可能要测很多次。比如“查询每门课程成绩最高的学生”,如果写成WHERE score = (SELECT MAX(score) FROM score WHERE course_id = ...),执行逻辑是每行都去子查询里求一次最大值,数据量上去以后性能很差。换用窗口函数或自连接,可能更快。刷题时要养成对比不同写法的习惯,不要只会一种。
3.2 索引优化:练习题不该只停留在“能跑通”
进阶题里我专门留一个模块叫“慢查询练习”。先造一张没有索引的百万行表,再让学员实现某个条件查询,然后优化到毫秒级。这个练习会涉及CREATE INDEX、复合索引、最左前缀原则、覆盖索引、索引下推。很多人在网上搜“mysql数据库命令大全”,但命令背得再多,不知道什么时候建索引等于白搭。练习时一定要学会看EXPLAIN的type、key、rows、Extra这几列。如果出现Using filesort或者Using temporary,说明排序或分组没有利用上索引,这是最常见的性能问题。
复合索引字段顺序怎么排?简单原则:等值条件放前面,范围条件放后面;区分度高的字段放前面。举个例子,查询条件经常是status = ? AND create_time < ?,那么索引可以建(status, create_time)。这个顺序能让等值定位先缩小范围,再用索引的有序性处理范围条件。如果反了过来,范围条件会让后面的等值条件失去索引优势。
练习题里还可以设置一个场景:用WHERE score = 90查成绩,但score表数据量很大,这时有两种选择,一是给score字段加普通索引,二是建立(score, student_id)的复合索引。后者能做覆盖索引,因为查询只需要返回成绩和学生ID,可以直接从索引里拿数据,不需要回表。通过EXPLAIN看Extra列是不是Using index,就能直观感受两种方案的差距。这个模块练完,再去看生产环境里的慢查询日志,你会一下子明白很多DBA的决策逻辑。
3.3 存储过程与触发器:流程控制和错误处理
进阶练习题里必须安排存储过程和触发器,因为这是很多人在面试中说不清楚的话题。存储过程练习从最简单的无参数过程开始:写一个存储过程,入参学生ID,返回该学生的平均成绩。这里重点考OUT参数、局部变量、IF判断、循环、游标。MySQL默认以分号作为语句结束符,但存储过程体里也有分号,所以必须用DELIMITER //临时改掉结束符。热搜词里“mysql中触发器中分隔符”问的就是这个,别人查来查去半小时,你只要记住一条原则:看到CREATE PROCEDURE或CREATE FUNCTION之前,先换DELIMITER。
触发器练习可以做:向成绩表插入数据后,自动更新学生表的总学分。创建触发器时要清楚时机,AFTER INSERT对应插入后,BEFORE UPDATE对应更新前。NEW和OLD伪表要记牢,INSERT只有NEW,DELETE只有OLD,UPDATE两者都有。触发器最大的坑是行为隐蔽,容易造成递归和性能问题,比如在成绩表上写一个触发器,触发器里又更新成绩表,就可能无限循环。所以练习题里我会让学员写一个能用的触发器,再故意写一个会递归的例子,看看MySQL报什么错,印象会更深。
错误信息处理也不能跳过。存储过程里要加DECLARE EXIT HANDLER FOR SQLEXCEPTION,这样发生异常时可以回滚事务,并输出错误信息。这正好对应热搜词“mysql储存过程+错误信息”。实操时故意传一个不存在的外键,让插入失败,看看错误处理有没有生效。如果整个过程只写快乐路径,不做异常处理,那和写一段一次性脚本没有任何区别,算不上真正的存储过程开发。
3.4 锁与事务隔离级别:并发场景练习题
锁和事务是进阶题里的硬骨头,不能只背理论。建议用两个终端连接同一个MySQL实例,终端A修改某行但不提交,终端B再去修改同一行,你会看到B一直等待,直到超时或A提交。这个练习能直观感受共享锁、排他锁、锁等待和死锁。再用SHOW ENGINE INNODB STATUS;查看锁信息,能帮你理解死锁检测是怎么回事。很多人搜“mysql锁表”,基本都是生产环境碰到阻塞才来查,其实提前模拟一次,以后就不会慌。
事务隔离级别方面,可以做“不可重复读”和“幻读”的对照实验。在READ COMMITTED和REPEATABLE READ两种级别下,分别执行同一组操作:事务A先查询某班级成绩,事务B插入一条新成绩并提交,事务A再查询一次,看看结果是否变化。MySQL默认是REPEATABLE READ,它的可重复读主要是靠MVCC版本链实现的,所以一次事务内多次查询能看到一致快照。这个模块练完,你就能理解为什么删除或插入操作容易产生“间隙锁”,以及为什么高并发下会出现死锁。
锁和事务是密不可分的。练习题中可以先写一个无索引条件的UPDATE,导致锁范围扩大,再查看锁状态。生产环境中最常见的锁表原因就是UPDATE没有走索引,比如UPDATE score SET score = score + 1 WHERE student_name = '张三',如果student_name上没有索引,可能会锁住大量行甚至整表。练习时一定要把这种现象亲手复现出来,才能真正理解“给WHERE条件字段加索引”这句话的分量。
3.5 窗口函数与排名问题:面试加分项
MySQL 8.0开始支持窗口函数,比如ROW_NUMBER、RANK、DENSE_RANK、NTILE、LAG、LEAD。这是一类高频面试题:按成绩排名,相同分数并列,但下一名跳过还是连续。基础写法是用用户变量@rank,但8.0里面推荐用窗口函数。练习题设计:查询每门课的前三名,这是一个分组TopN问题,用ROW_NUMBER() OVER (PARTITION BY course_id ORDER BY score DESC)非常清晰。
老版本MySQL没有窗口函数,只能用关联子查询或变量实现。我会特意让学员两种都写一遍,因为实际生产环境里你可能会碰到老版本。变量写法的核心是:在子查询中先按课程和分数排序,再用变量记录同一个课程内的序号。这个写法虽然笨,但能让你理解ORDER BY的执行顺序,窗口函数多了反而会掩盖这些内部细节。窗口函数刷上几题之后,你会发现很多复杂问题都能用一两个OVER子句解决,比如同比环比、连续登录天数、分组TopN,这些都是面试题库里的常客。
LAG和LEAD函数也值得专门练,它们能访问当前行之前或之后的行,非常适合做“连续出现”类题目。比如查询成绩表中连续出现3次及以上的分数,就可以用LAG(score, 1)和LAG(score, 2)来判断。这种题没有窗口函数时写起来很绕,有了窗口函数就清晰很多。面试时你如果能主动提到窗口函数,并比较RANK、DENSE_RANK、ROW_NUMBER的区别,面试官通常会觉得你是有实战经验的。
3.6 视图、临时表与CTE:提升SQL组织能力
进阶阶段还要练视图、临时表和公用表表达式(CTE)。视图本质上是一段保存下来的SQL查询,它不存储数据,只是方便重复使用。练习题可以设计:创建一个视图,用于展示学生姓名、课程名、成绩以及成绩等级。之后所有涉及这些字段的查询都可以直接查视图,代码看起来清爽很多。但视图也有坑,某些复杂查询在视图上再查询,可能会叠加多层嵌套,性能反而下降,所以练习时要把视图和直接写SQL的EXPLAIN对比一下。
临时表是会话级的,连接断开就自动删除。在多步计算中,临时表能把一个大问题拆成几个小步骤。比如先查出每个学生的总分,再判断总分排名,可以先把第一步结果放到临时表,然后再和其他表关联。临时表练得好,写复杂报表时会轻松很多。CTE是MySQL 8.0里更优雅的替代方案,用WITH语句定义公共表达式,可以避免多层子查询嵌套。特别是递归CTE,比如生成数字序列、遍历部门层级,非常实用。练习题里可以来一道:用递归CTE生成1到10的数字序列,虽然简单,但能让你理解递归终止条件和UNION ALL的用法。
4. 一套完整的学生课程成绩练习题实战
4.1 需求分析与表结构设计
我拿一套最经典的案例来练手:学生课程成绩数据库。需求是记录学生基础信息、课程信息、学生选课和考试成绩。需要几张表?最合理的是三张:学生表、课程表、成绩表。成绩表作为关联表,存储student_id、course_id、score,并建立联合唯一约束,防止同一学生同一课程重复录入成绩。这样的设计能保证数据一致性,也符合第三范式要求。
表结构设计时考虑的因素很多。学生表要存学号、姓名、性别、出生日期,学号应该使用唯一索引。课程表要存课程名、学分、授课教师。成绩表存学生ID、课程ID、成绩、考试时间。主键统一用自增INT,成绩用DECIMAL(5,2)。考试时间有默认值CURRENT_TIMESTAMP。为了模拟真实业务,我会给成绩表加外键,虽然生产环境很多团队为了性能会去掉外键,但练习题里保留它很有价值,因为你能观察到外键约束对INSERT的影响,也能体会到DELETE父表记录时会被外键拦截。
4.2 建表语句与初始化数据
下面给出可直接运行的建表和数据脚本,建议在本地MySQL 8.0环境执行,工具用Navicat、DBeaver或命令行都行。
sql复制CREATE DATABASE IF NOT EXISTS school CHARSET utf8mb4;
USE school;
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
stu_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号',
name VARCHAR(50) NOT NULL,
gender CHAR(1) DEFAULT 'M',
birth_date DATE,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB COMMENT='学生表';
CREATE TABLE course (
id INT PRIMARY KEY AUTO_INCREMENT,
course_name VARCHAR(100) NOT NULL,
credit DECIMAL(3,1) NOT NULL DEFAULT 0,
teacher VARCHAR(50)
) ENGINE=InnoDB COMMENT='课程表';
CREATE TABLE score (
id INT PRIMARY KEY AUTO_INCREMENT,
student_id INT NOT NULL,
course_id INT NOT NULL,
score DECIMAL(5,2) NOT NULL,
exam_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_stu_course (student_id, course_id),
CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id),
CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id)
) ENGINE=InnoDB COMMENT='成绩表';
上面这张成绩表故意加了外键和唯一索引,练习插入重复数据时,就会触发Duplicate entry错误。如果不想外键影响练习题性能,可以在生产环境去掉外键,但练习题里保留它有用。
初始化数据,我一般插入4门课和10个学生,成绩用存储过程生成。下面这个存储过程也正好是进阶题的一部分:
sql复制DELIMITER //
CREATE PROCEDURE generate_score(IN student_count INT, IN course_count INT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE j INT DEFAULT 1;
START TRANSACTION;
WHILE i <= student_count DO
SET j = 1;
WHILE j <= course_count DO
INSERT INTO score(student_id, course_id, score) VALUES (i, j, ROUND(RAND()*100,2));
SET j = j + 1;
END WHILE;
SET i = i + 1;
END WHILE;
COMMIT;
END //
DELIMITER ;
CALL generate_score(10, 4);
执行完可以看看student和score表行数,再顺手练一下统计行数、去重数和平均数。如果想用真实姓名来练LIKE查询,也可以手写INSERT语句,插入几个姓“张”的学生,方便后面题目验证。
4.3 10道典型练习题及解析
这套题目由浅入深,基本覆盖了基础到进阶的所有高频考点。
- 查询所有学生的学号、姓名和性别,按学号升序。考察基础SELECT和ORDER BY。
- 查询姓“张”的学生有哪些。考察LIKE、通配符、前缀匹配。
- 查询每门课程的平均成绩,按平均分从高到低排序。考察GROUP BY、聚合函数、ORDER BY。
- 查询平均成绩大于等于85分的学生姓名。考察JOIN + GROUP BY + HAVING。
- 查询选修了“数学”课程但成绩低于60分的学生。考察多表关联或子查询。
- 查询没有任何选课记录的学生。考察LEFT JOIN + IS NULL或NOT EXISTS。
- 查询每门课程成绩最高的学生姓名和分数。考察组内极值问题。
- 查询总成绩排名前三的学生,如果分数相同则并列。考察窗口函数RANK/DENSE_RANK。
- 将成绩低于50分的记录全部加5分,但不能超过60分。考察UPDATE+CASE WHEN。
- 查询成绩表中连续出现3次及以上的分数。考察自连接或窗口函数LAG。
每一道题都建议先自己写,再对比答案。尤其是第7题,它的解法很多,答案差异很大。我先给窗口函数写法:
sql复制SELECT t.student_id, t.course_id, t.score, t.name
FROM (
SELECT s.id, s.student_id, s.course_id, s.score,
stu.name,
ROW_NUMBER() OVER (PARTITION BY s.course_id ORDER BY s.score DESC) rn
FROM score s
JOIN student stu ON stu.id = s.student_id
) t
WHERE t.rn = 1;
如果不用窗口函数,用相关子查询也能写:
sql复制SELECT s.student_id, s.course_id, s.score, stu.name
FROM score s
JOIN student stu ON stu.id = s.student_id
WHERE s.score = (
SELECT MAX(s2.score)
FROM score s2
WHERE s2.course_id = s.course_id
);
但要注意,如果同一个课程有多个最高分相同的学生,第二条会返回多行,第一条只会返回一个。练习时就要对比这种差异,面试官最爱在这里挖坑。
第10题“连续出现3次及以上的分数”标准写法,可以按成绩表id顺序判断,也可以按考试时间排序。更通用的写法是用LAG:
sql复制SELECT DISTINCT score
FROM (
SELECT score,
LAG(score, 1) OVER (ORDER BY id) AS prev_score,
LAG(score, 2) OVER (ORDER BY id) AS prev_score2
FROM score
) t
WHERE score = prev_score AND score = prev_score2;
这里假设记录顺序由id决定,实际业务中如果有考试时间字段,应该用exam_time排序,所以建表时保留exam_time字段就很关键。
4.4 参考答案与验证要点
第1题答案很简单,SELECT stu_no, name, gender FROM student ORDER BY stu_no ASC;,验证要点是结果集顺序正确。第2题答案是SELECT * FROM student WHERE name LIKE '张%';,要注意如果排序规则区分大小写,可能影响个别边界值,不过一般不影响。第3题和第4题都要注意HAVING位置,第3题是先GROUP BY课程,再ORDER BY平均分;第4题是先JOIN再GROUP BY,然后用HAVING过滤平均分,不能把HAVING条件写到WHERE里。
第5题的参考答案可以先找到“数学”的course_id,再关联成绩表,也可以用子查询。我推荐写成显式JOIN,可读性更好。第6题参考答案:
sql复制SELECT s.id, s.name
FROM student s
LEFT JOIN score sc ON sc.student_id = s.id
WHERE sc.student_id IS NULL;
很多新手会写成JOIN score,结果查出了有成绩的学生,这就是没有理解LEFT JOIN的语义。第8题用DENSE_RANK可以处理并列排名,而RANK会跳号,面试常问二者区别。第9题参考答案就是上一节写过的CASE WHEN更新语句。第10题如果数据量小,可以肉眼核对;数据量大时一定要看窗口函数的排序字段是否正确,否则结果不对。
验证SQL结果最简单的办法:把参考答案结果和关键行数记录下来,比如第3题平均分课程数应该是4行;第6题没有选课的学生数需要根据初始化数据自己算。我经常用SELECT COUNT(*)确认影响行数,避免“看起来对了,实际上多一条少一条”。练习题不是写出来就行,一定要主动设计边界条件,比如空表、重复数据、NULL值,这些才是实际业务里最折磨人的地方。
4.5 如何把练习题变成面试题
同一个练习题,增加一个追问就能从基础题变成面试题。比如第7题“每门课程成绩最高的学生”,如果面试官继续问:如果一门课有两个最高分,都需要查出来,你的SQL还正确吗?这时候窗口函数ROW_NUMBER会漏数据,需要换成RANK或者DENSE_RANK。所以刷题时不能只满足于写出来,还要想“如果需求变化怎么办”。
再比如第3题“统计每门课程平均分”,可以继续问:如果平均分只统计及格记录,该加WHERE还是HAVING?答案是WHERE score >= 60放在分组前过滤,如果写在HAVING里,分组结果已经被全部成绩影响,数据就不对。这种变化才是面试题的精髓。我会在刷题时给每道练习写至少两个变体:一个是数据条件变化,一个是排序或分组维度变化。把一道题练到能应对三次追问,比囫囵吞枣写十道题强得多。
第10题也可以变成“连续登录N天”的进阶题,需要用到LAG和日期减法。把成绩表换成登录表,score字段换成login_date,解题思路是一样的。这种迁移能力,才是从“会写SQL”到“会做数据查询设计”的关键分水岭。
5. 刷题过程中的常见问题与排查技巧实录
5.1 环境问题:安装、连接、认证失败
MySQL练习最大阻碍往往不是SQL,而是环境起不来。很多人在Windows安装MySQL时卡在“Configuration of MySQL server is taking”,或者启动服务报错。遇到这种情况,优先检查端口3306是否被占用,安装目录是不是中文路径,服务账号权限是否正常。如果是端口被占,可以改端口,也可以先netstat -ano | findstr 3306找到占用进程后决定要不要换端口。热搜词“mysql端口号”频繁出现,说明很多人在连接阶段就卡住了。
连接工具报错“Firedac phys mysql client does not support authentication protocol requested”,本质上是MySQL 8.0默认使用caching_sha2_password插件,而老客户端不支持。解决办法有两个:一是升级客户端驱动,二是在MySQL里把用户的认证插件改回mysql_native_password:
sql复制ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';
FLUSH PRIVILEGES;
这个坑几乎每个新手都会踩,我在练习题环境准备里会专门放这段话。如果你用Docker安装MySQL,也别忘了映射端口和数据卷,容器重启后数据还在依赖数据卷,这和本地安装一样重要。环境不稳,后面所有练习题都会受影响,所以先把一根笔直的跑道铺好,再开始跑。
5.2 SQL执行报错:语法、排序规则、类型转换
刷题时报错比例最高的一
