SQL 100道基础练习题:从入门到实战的系统训练指南
2026/9/11 3:38:54 网站建设 项目流程

上周有个刚转行的朋友问我:“我把网上的SQL题刷了两百多道,可一碰到需求稍微变一下的查询还是卡壳,问题出在哪?”我看了看他刷的题,答案其实很简单——他刷的是“题”,不是“方法”。

这让我想起自己做MySQL教学和面试辅导时一直在用的那套“SQL 100道基础练习题”。它不追求偏题怪题,而是把SQL日常开发里最常见的场景拆成一个个可递进的练习:从单表过滤开始,一步步走到聚合分组、多表连接、子查询、窗口函数,最后落到DML、索引和事务。每一道题背后都有一个明确的考察点,刷完不是让你“见过”,而是让你以后拿到任何需求都能在脑子里先画出一条SQL的骨架。

这篇文章把完整题目分类、表结构设计、部分典型题的精讲答案,以及我自己在批改练习时看到的高频错误全部整理出来。适合三类人:刚学完SQL语法但不知道怎么实战的初学者;准备数据分析师或后端开发面试、想在短时间内系统过一遍SQL重点的求职者;以及基础不牢、想回头补课的在职开发者。

1. 这套题不是让你背答案的:题目的设计逻辑

市面上的SQL题很多,但大多存在两个问题:一是题目之间没有递进关系,东一榔头西一棒槌;二是过度堆砌复杂场景,初学者连表结构都看不懂就直接被劝退。我最初整理这100道题时,设定的原则只有三条:覆盖日常工作高频语法、难度循序渐进、每道题都必须能讲清楚“为什么这样写”。

1.1 为什么要从三张表开始

很多教程喜欢用几十张表的大业务模型来出题,但我个人强烈不建议新手这么做。SQL的核心思维是“对集合进行操作”,这个思维在简单的表结构上更容易建立起来。这套题只用了三张表:学生表、课程表、成绩表,三者之间有外键关联,既有单表操作,又能做多表连接,还能支持子查询和窗口函数的大部分练习场景。等你把这三张表玩透了,换任何复杂的业务表结构,本质上都是同样的套路。

另一个现实原因是,我在面试候选人和帮同事review代码时发现,不少人写了大半年业务SQL,看到“LEFT JOIN之后数据变多”“GROUP BY之后查不到非分组字段”这样的问题还是懵的。这些基础问题的根源,恰恰是对单表过滤和连接逻辑的理解不够扎实。100道题里我特意把连接和子查询的部分占比拉高,因为这两块才是实战中真正拉开差距的地方。

1.2 100道题怎么分配

整套题我分成了六个模块,每个模块有明确的训练目标,不是平均用力:

模块题目数量训练目标
基础查询与过滤10题掌握SELECT核心子句、WHERE条件、LIKE/IN/BETWEEN/NULL判断
聚合、分组与排序20题理解分组聚合逻辑,能区分WHERE与HAVING
多表连接25题掌握INNER JOIN/LEFT JOIN用法、自连接与连接条件
子查询与派生表20题学会把复杂查询拆解成独立子问题
窗口函数15题掌握排名、累计、分组内计算等进阶语法
DML、视图、索引与事务10题不只会查,还要能安全地增删改和建索引

后面每个模块我都会有题目清单和精讲。你可以按顺序刷,也可以根据面试目标跳着刷,但我不建议打乱模块内部的顺序,因为每道题考察的语法点都是在前面的基础上叠加的。

2. 动手前先把三张表和测试数据建好

练SQL的前提是你手上有一套干净的、自己完全掌控的数据。我见过有人直接用生产库练习,结果一条UPDATE把线上数据改了,这个风险无论如何都要避免。我建议你在自己的电脑上装一个MySQL 8.0,用下面的脚本建库建表。

2.1 环境准备

装MySQL很简单,直接到官网下载MySQL Community Server,安装时记住root密码。如果你追求效率,也可以用Docker:

docker run --name mysql100 -e MYSQL_ROOT_PASSWORD=yourpassword -p 3306:3306 -d mysql:8.0

客户端方面,命令行效率太低,我平常用Navicat或MySQL Workbench。Workbench免费但有时候界面卡顿,Navicat需要破解,经济条件允许的话建议支持正版。其实有一个更轻量的选择:VS Code装一个MySQL插件,日常练习完全够用,重点是能用快捷键执行选中SQL,比在终端一行行敲舒服得多。

2.2 建表语句

这100道题全部围绕学生选课成绩这个场景展开,三张表的结构如下:

CREATE DATABASE IF NOT EXISTS sql100 DEFAULT CHARSET utf8mb4; USE sql100; DROP TABLE IF EXISTS scores; DROP TABLE IF EXISTS students; DROP TABLE IF EXISTS courses; CREATE TABLE students ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '学生ID', name VARCHAR(50) NOT NULL COMMENT '姓名', gender CHAR(1) DEFAULT 'M' COMMENT '性别 M/F', class VARCHAR(20) COMMENT '班级', age INT COMMENT '年龄', enroll_date DATE COMMENT '入学日期' ) ENGINE=InnoDB COMMENT='学生表'; CREATE TABLE courses ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '课程ID', course_name VARCHAR(100) NOT NULL COMMENT '课程名称', teacher VARCHAR(50) COMMENT '授课老师', credit DECIMAL(3,1) COMMENT '学分' ) ENGINE=InnoDB COMMENT='课程表'; CREATE TABLE scores ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) COMMENT '成绩', exam_date DATE COMMENT '考试日期', CONSTRAINT fk_scores_student FOREIGN KEY (student_id) REFERENCES students(id), CONSTRAINT fk_scores_course FOREIGN KEY (course_id) REFERENCES courses(id) ) ENGINE=InnoDB COMMENT='成绩表';

这里有几个细节想提醒你。第一,所有字符集统一用utf8mb4,避免中文乱码;第二,成绩字段用DECIMAL(5,2)而不是FLOAT,因为浮点数在比较时会有精度问题,这在后面做范围查询时会让结果变得莫名其妙;第三,外键约束在建表时就加上,练习子查询时你才能理解为什么有些“无关联”的数据会被LEFT JOIN带出来。

2.3 插入测试数据的几个小技巧

建完表要插数据。为了让练习效果更好,我建议你至少插入20名学生、6门课程、60条成绩记录,并且要刻意制造一些“陷阱数据”:比如某个学生没有任何选课记录、某门课程没有学生选、某些成绩为NULL、两个学生同姓名等。这些脏数据,后面很多题目就是专门针对它们出的。

INSERT INTO courses (course_name, teacher, credit) VALUES ('语文', '王老师', 3.0), ('数学', '李老师', 4.0), ('英语', '张老师', 2.0), ('物理', '李老师', 4.0), ('化学', '赵老师', 3.0), ('生物', '孙老师', 2.5); INSERT INTO students (name, gender, class, age, enroll_date) VALUES ('张伟', 'M', '一班', 20, '2022-09-01'), ('王芳', 'F', '一班', 19, '2022-09-01'), ('李娜', 'F', '二班', 21, '2021-09-01'), ('刘洋', 'M', '二班', 20, '2021-09-01'), ('陈静', 'F', '三班', 22, '2020-09-01'), ('杨磊', 'M', '三班', 21, '2020-09-01'), ('赵敏', 'F', '一班', 20, '2022-09-01'), ('孙浩', 'M', '二班', 22, '2021-09-01');

我特意让“张伟”和“赵敏”没有出现在scores表里,后面题目会用到。成绩记录你可以自己造,原则是保证每个学生至少选了1门课、最多选5门课,并且把班级区分开,方便做分组聚合练习。

3. 模块一:基础查询与过滤,先把SELECT的手感练出来

很多人觉得SELECT太简单,但面试里最容易失分的恰恰是条件边界。什么叫“年龄大于20岁”?如果age字段允许为NULL,那么age > 20这个条件会把NULL的记录过滤掉,而很多人根本没意识到NULL的存在。这批基础题就是为了把这些细节磨透。

题号题目考察点
1查询学生表中所有学生的全部信息SELECT基本语法
2查询所有学生的姓名和班级投影列
3查询年龄大于20岁的学生姓名和年龄WHERE数值比较
4查询一班且性别为F的学生AND多条件
5查询姓名以“张”开头的学生LIKE模糊匹配
6查询年龄在18到22岁之间的学生BETWEEN边界
7查询班级为一班或二班的学生IN列表
8查询没有填写年龄的学生IS NULL
9查询学生表一共有多少条记录COUNT(*)
10查询一共有哪些班级DISTINCT去重

精讲两道比较容易出错的。

第5题:查询姓名以“张”开头的学生。

SELECT * FROM students WHERE name LIKE '张%';

考察点是LIKE通配符。%匹配任意多个字符,_匹配单个字符。实际业务中经常遇到用户输入一个姓来搜索,这里的索引利用率其实是很多人忽略的:如果name字段有前缀索引或普通索引,'张%'这种写法是可以走索引的,但'%张'不行。你写SQL时心里要有个弦,%放在最前面的模糊查询,数据量一上来就容易慢。

第8题:查询没有填写年龄的学生。

SELECT * FROM students WHERE age IS NULL;

这个必须用IS NULL,不能用= NULL。在SQL里NULL不是一个值,而是“未知”的标记,任何与NULL做比较的结果都是NULL,也就是说age = NULL永远不会为TRUE。我在面试中问过很多候选人,至少有三分之一会写成age = NULLage = '',这两个都拿不到正确结果。如果真的想查空字符串,才用age = '',但NULL与空字符串是两个完全不同的概念,造数据时也要注意区分。

第2题的变形——别名:

SELECT name AS 姓名, class AS 班级 FROM students;

AS可以省略,但建议保留,可读性更好。注意中文别名在命令行客户端不需要加引号,在部分框架里可能报错,保险起见可以写成'姓名'

4. 模块二:聚合分组与排序,别一上来就写窗口函数

进入第11题到第30题,这是整套练习题的第一次卡人点。很多初学者能在SELECT和WHERE里游刃有余,一遇到GROUP BY就蒙。核心原因是没有建立“分组后每一行代表一个组”这个认知。聚合函数的作用对象不是某一条记录,而是一个分组,分组里可能包含多条记录,但最终只输出一行。

题号题目考察点
11查询每个班级有多少名学生GROUP BY + COUNT
12查询每个班级学生的平均年龄GROUP BY + AVG
13查询男女生各有多少人分组多字段
14查询每门课程的平均分、最高分、最低分多个聚合函数
15查询每个学生选了多少门课COUNT + GROUP BY
16查询平均分大于80分的课程HAVING
17查询选课人数超过5人的课程IDHAVING + COUNT
18查询每个班级中年龄最大的学生年龄MAX + GROUP BY
19查询学生总数按班级降序排列ORDER BY + 别名
20查询每门课程的平均分,并按照平均分从高到低排序聚合 + 排序
21查询成绩总分最高的学生IDSUM + GROUP BY
22查询年龄在20岁以上的学生各班人数WHERE + GROUP BY
23查询每个班级男女生各多少人GROUP BY多列
24查询班级人数不足3人的班级HAVING
25查询每门课的选课人数,没有学生选的课也要显示稍后用JOIN解决
26查询各班级学生的年龄总和SUM
27查询最小年龄、最大年龄和平均年龄聚合无分组
28查询姓名重复的学生及重复次数GROUP BY + HAVING
29查询每个班级最早入学日期MIN
30按年龄排序,年龄相同按入学日期排序多字段排序

第16题:查询平均分大于80分的课程。

SELECT course_id, AVG(score) AS avg_score FROM scores GROUP BY course_id HAVING AVG(score) > 80;

这道题十个人有八个人会写错,错误版本是:

-- 错误示例 SELECT course_id, AVG(score) FROM scores WHERE AVG(score) > 80 GROUP BY course_id;

错在WHERE不能使用聚合函数。WHERE是在分组之前对原始行进行过滤的,而聚合函数的结果在分组之后才产生。HAVING的作用就是过滤分组。一句话总结:WHERE过滤行,HAVING过滤组。另外,HAVING中尽量直接写聚合函数,不要依赖SELECT中的别名,有些数据库不支持在HAVING里引用别名。

第28题:查询姓名重复的学生及重复次数。

SELECT name, COUNT(*) AS cnt FROM students GROUP BY name HAVING COUNT(*) > 1;

这是面试常考的“查重复记录”原型题,它能变形出很多类型:查手机号重复、查订单号重复、查同一用户同一天的重复登录等。核心就是GROUP BY后COUNT大于1。

5. 模块三:多表连接,SQL面试最常见的分水岭

从第31题到第55题是整套题里含金量最高的部分。多表连接不是背语法,而是要理解“连接到底在干什么”。我教新人的时候常用一个比喻:JOIN就是把两张表按某种匹配规则横向拼接起来,匹配上的行合在一起成为新表的一行;匹配不上的行,如果是LEFT JOIN,左边表的行会保留,右边表的字段填NULL。

题号题目考察点
31查询学生姓名和其选课的课程名称INNER JOIN三表
32查询所有学生的选课情况,没选课的学生也要显示LEFT JOIN
33查询所有课程的学生选课情况,没被选的课程也要显示RIGHT JOIN或LEFT JOIN换表
34查询没有选任何课程的学生LEFT JOIN + IS NULL
35查询没有被任何学生选择的课程LEFT JOIN + IS NULL
36查询每个学生的选课数量JOIN + GROUP BY
37查询每门课程的选课学生人数同上
38查询选修了“语文”课的学生的姓名JOIN + WHERE
39查询“李老师”教的所有课程以及选课人数JOIN + 聚合
40查询每个班级中每门课程的平均分JOIN + GROUP BY多列
41查询学生姓名、课程名、成绩,并按成绩降序排列JOIN + ORDER BY
42查询成绩大于90分的学生姓名和课程名JOIN + WHERE
43查询每位同学的平均分,并显示姓名JOIN + GROUP BY
44查询同班同名的学生自连接
45查询年龄比同班平均年龄小的学生衍生表+自连接
46查询至少选了两门课的学生姓名JOIN + HAVING
47查询每个学生各科成绩都大于60分的记录聚合+比较
48查询所有学生中与“王芳”同班的学生自连接
49查询每门课最高分对应的学生姓名连接 + 子查询
50查询每个学生总分的排名先SUM后JOIN再排序
51查询没学过“李老师”课程的学生NOT IN + JOIN
52查询选课门数超过3门的学生姓名HAVING
53查询各科平均分都在80分以上的学生IDHAVING + MIN
54查询总成绩排名前3的学生姓名派生表
55查询所有课程成绩都大于等于90分的学生NOT EXISTS

第34题:查询没有选任何课程的学生。

SELECT s.id, s.name FROM students s LEFT JOIN scores sc ON s.id = sc.student_id WHERE sc.id IS NULL;

这道题是“LEFT JOIN + 右表IS NULL”的经典用法,比用NOT IN子查询在数据量大时性能更稳定。需要注意的是ON后面的连接条件到底该带什么、WHERE中过滤右表字段时会不会把左表记录也滤掉。如果你写了WHERE sc.student_id IS NULL,结果一样;但如果你不小心在WHERE里加了一个AND sc.score > 0,那NULL记录全部被过滤掉,结果就变成0行了。这种错误我见过不止一次。

第44题:查询同班同名的学生。

SELECT a.name, a.class FROM students a JOIN students b ON a.name = b.name AND a.class = b.class AND a.id <> b.id;

自连接的要点是给同一张表起两个不同的别名,关键条件要写a.id <> b.id,否则每对重复姓名会和自己匹配一次。同理,查询“比同班同学年龄大”就是ON a.class = b.class AND a.age > b.age,这时候没有id <>条件也会自动排除自己,因为自己的年龄不可能大于自己。

**第49题:查询每门课最高分对应的学生姓名。**这道题很多人用GROUP BY + MAX先查出最高分,再JOIN成绩表。但要注意,如果同一门课有两个学生同分且都是最高分,分组+JOIN会得到两行,这其实是正确的。如果题目改成“每门课最高分的学生”,语义上可以允许多个最高分。想取其中一个,就需要窗口函数或ORDER BY + LIMIT,这是后话。

6. 模块四:子查询与派生表,把复杂问题拆碎

第56题到第75题是子查询专项。子查询的核心价值在于把“一步到位”写不出来的查询,拆成“先查一个结果集,再基于结果集查询”。写子查询不丢人,可读性永远比“用极其复杂的方法强行避免子查询”更重要。真正需要优化的时候,再考虑改成JOIN或EXISTS。

题号题目考察点
56查询与“张伟”同班级的学生IN子查询
57查询年龄大于全校平均年龄的学生标量子查询
58查询选了“语文”课的学生姓名IN子查询
59查询没有选“数学”课的学生姓名NOT IN
60查询成绩表中存在成绩记录的学生IDEXISTS
61查询不存在成绩记录的学生姓名NOT EXISTS
62查询每门课成绩最高的学生和成绩相关子查询
63查询比所在班级平均年龄小的学生相关子查询
64查询选修课程数超过平均选课门数的学生子查询+HAVING
65查询平均分最高的学生姓名派生表
66查询每个班级年龄最小的学生相关子查询
67查询总分超过班级平均总分的学生两次聚合的嵌套
68查询每门课的选课人数并显示课程名派生表JOIN
69查询所有学生中年龄最大的学生信息标量嵌套
70查询成绩表中比某门课程平均分高的记录多个子查询
71查询每个学生分数最高的课程相关子查询
72查询成绩排名第二的学生的姓名和成绩ORDER BY + LIMIT
73查询选了所有课程的学生双重NOT EXISTS
74查询学生姓名及各科成绩中最高分关联子查询
75查询同一年级中比同班任一同学年龄都大的学生ALL子查询

第57题:查询年龄大于全校平均年龄的学生。

SELECT name, age FROM students WHERE age > (SELECT AVG(age) FROM students);

这是一个标量子查询,返回结果是单个值。注意AVG(age)如果students表是空表会返回NULL,那么WHERE条件就恒为NULL,查不出任何行。实际开发中,统计报表脚本一定要处理这种空表场景,否则结果会让你排查很久。

第65题:查询平均分最高的学生姓名。

SELECT s.name, t.avg_score FROM students s JOIN ( SELECT student_id, AVG(score) AS avg_score FROM scores GROUP BY student_id ORDER BY avg_score DESC LIMIT 1 ) t ON s.id = t.student_id;

派生表就是FROM后面的子查询,它必须有一个别名。MySQL在8.0之前对派生表的优化很差,8.0之后有自动物化和合并优化,性能改善明显。如果你还在用5.7,遇到派生表慢的情况,可以先STRAIGHT_JOIN或者改成临时表。

第73题:查询选了所有课程的学生。

SELECT s.id, s.name FROM students s WHERE NOT EXISTS ( SELECT c.id FROM courses c WHERE NOT EXISTS ( SELECT 1 FROM scores sc WHERE sc.student_id = s.id AND sc.course_id = c.id ) );

这题是双重否定:选出那些“不存在任何一门课他没有选”的学生。很多初学者看到答案直接懵了,但你只要掌握一个方法论:把SQL从外层往内层读,每个NOT EXISTS都翻译成“不存在……的记录”,语境就清晰了。这也是数据库面试里出镜率极高的一道题,值得反复研究。

7. 模块五:窗口函数与进阶语法,面试加分项

从第76题到第90题是窗口函数专项。MySQL 8.0之后开始支持窗口函数,这算是目前面试中非常热门的考点。窗口函数和GROUP BY最大的区别是:GROUP BY会折叠多行为一行,而窗口函数不折叠行,它在每一行上都保留原有的行,同时增加一个计算列。

题号题目考察点
76按成绩从高到低给所有成绩记录排名ROW_NUMBER
77查询每门课成绩的前三名RANK + PARTITION BY
78查询每个学生总分在中位数以上的记录PERCENT_RANK
79查询每门课最高分与最低分的分差MAX/MIN OVER
80查询每个班级学生按年龄排序的名次DENSE_RANK
81查询每位同学在各科考试中的累计总分SUM OVER
82查询每位同学成绩相比上一次考试的变化LAG
83查询每位同学成绩相比下一次考试的变化LEAD
84查询每个班级年龄最大的学生ROW_NUMBER + PARTITION
85查询每门课平均分排名AVG OVER
86查询每个学生的选课数量在所有学生中的排名COUNT OVER ORDER BY
87查询每个学生成绩最好的课程名称窗口函数 + JOIN
88查询每个班级按总分排名的前三名两层窗口
89查询成绩表中每行与其所在课程平均分的差值AVG OVER
90查询每个学生最近一次考试成绩ROW_NUMBER + PARTITION + ORDER BY

第77题:查询每门课成绩的前三名。

SELECT course_id, student_id, score FROM ( SELECT course_id, student_id, score, ROW_NUMBER() OVER (PARTITION BY course_id ORDER BY score DESC) AS rn FROM scores ) t WHERE rn <= 3;

这里用ROW_NUMBER而不是DENSE_RANK,两者的区别是:ROW_NUMBER对并列成绩也会给出不同的序号(1、2、3、4),而DENSE_RANK会为相同成绩分配相同名次,并且下一名是紧接着的数字(1、1、2)。如果你希望“成绩相同算并列”,就用DENSE_RANK。我建议你先把窗口函数中的三个排序函数都练一遍,然后回答一下它们之间的差异,这道面试送分题别丢。

第82题:查询每位同学成绩相比上一次考试的变化。

SELECT student_id, exam_date, score, LAG(score) OVER (PARTITION BY student_id ORDER BY exam_date) AS prev_score, score - LAG(score) OVER (PARTITION BY student_id ORDER BY exam_date) AS diff FROM scores;

LAG取同一分组内前一行,LEAD取后一行。PARTITION BY指定了分组范围,ORDER BY指定了组内的排序规则,这两者缺一不可。很多人在学习窗口函数时最大的误区是忘记写PARTITION BY,结果导致全表变成一个窗口,算出来的值完全不是想要的分组内结果。

窗口函数的执行顺序要记住一个口诀:窗口函数是在WHERE和GROUP BY之后执行的,所以你不能在WHERE里对窗口函数的结果进行过滤,这就是为什么第77题需要先查窗口函数再包一层子查询。如果你在MySQL 8.0里写了WHERE rn <= 3,会直接报错,因为WHERE子句根本看不到窗口函数生成的列。

8. 模块六:DML、视图、索引与事务,不只会查还要会改

第91题到第100题虽然只有10道,但它们保证了这套练习的完整性。很多学习SQL的人只会SELECT,一到要写UPDATE和DELETE就畏手畏脚,或者乱写一气导致生产事故。我个人认为,用可控的练习数据把DML、视图、索引和事务都过一遍,是非常必要的。

题号题目考察点
91向students表插入一条新学生记录INSERT基础
92将“张伟”的班级改为一班UPDATE单表
93删除年龄大于25且没有成绩记录的学生DELETE + JOIN
94创建一个视图,显示学生姓名、课程名、成绩CREATE VIEW
95从视图中查询平均分最高的学生视图复用
96给scores表的course_id字段创建索引CREATE INDEX
97用EXPLAIN查看一条查询语句的执行计划EXPLAIN
98开启一个事务,插入一条成绩记录并回滚BEGIN/ROLLBACK
99开启一个事务,插入一条记录并提交COMMIT
100查询当前数据库中的所有表和视图SHOW TABLES

第93题:删除年龄大于25且没有成绩记录的学生。

DELETE s FROM students s LEFT JOIN scores sc ON s.id = sc.student_id WHERE s.age > 25 AND sc.id IS NULL;

MySQL支持在DELETE中直接JOIN,这是很多人不知道的语法。执行前可以先改写成SELECT看看会命中哪些行,这是一个好习惯:

SELECT s.* FROM students s LEFT JOIN scores sc ON s.id = sc.student_id WHERE s.age > 25 AND sc.id IS NULL;

第97题:用EXPLAIN查看一条查询语句的执行计划。

EXPLAIN SELECT s.name, c.course_name, sc.score FROM scores sc JOIN students s ON sc.student_id = s.id JOIN courses c ON sc.course_id = c.id WHERE sc.score > 90;

看EXPLAIN主要关注type列和rows列,理论上type至少要到ref,如果出现ALL全表扫描且数据量大,就要考虑加索引。还要注意Extra列里有没有Using filesort和Using temporary,这两个都是SQL优化的重点信号。做这道题的时候可以试一下:把scores表的course_id索引先删掉再EXPLAIN,你会发现rows列的预估扫描行数明显上升,这样就直观理解索引的作用了。

第98题和第99题:事务回滚与提交。

START TRANSACTION; INSERT INTO students (name, gender, class, age) VALUES ('测试学生', 'F', '一班', 18); ROLLBACK; START TRANSACTION; INSERT INTO students (name, gender, class, age) VALUES ('保留学生', 'M', '二班', 19); COMMIT;

事务我建议用InnoDB练习,MyISAM不支持事务。执行ROLLBACK或COMMIT之后,再SELECT一下,你能直观看到哪些操作被保留、哪些被撤销。别看这两题简单,对新手理解“原子性”和“持久性”非常有帮助。实际工作中,如果你在存储过程或者脚本里做了多步数据修改,一定记得包在事务里,并且在异常分支做ROLLBACK,这是避免脏数据的最后一道防线。

9. 刷完这100道题之后,我实测容易翻车的五个点

这套题我在带新人、帮朋友突击面试时反复用了很多轮,最后总结几个绝大多数人都会踩的坑,提前写给你,省得你走弯路。

**第一,函数依赖问题。**MySQL 5.7和8.0中,SELECT后面出现的非聚合字段如果不是GROUP BY字段,默认是允许的(5.7只在严格模式下才报错),但查出来的值来自分组中的哪一行是不确定的。第16题如果写成SELECT course_id, course_name, AVG(score)且GROUP BY只写了course_id,course_name可能随便拿一行,逻辑上是错的。写SQL时自己心里要有数,别依赖这个“默认行为”。

**第二,JOIN后面的ON和WHERE的边界。**第34题已经演示过:ON负责连接条件,WHERE负责结果集过滤。如果你想在LEFT JOIN后把右边为NULL的行保留,过滤条件必须写在ON里而不是WHERE里。同理,在内连接中ON和WHERE的最终结果等价,但语义不同,别混着用。

**第三,COUNT(1)还是COUNT(*)。**这两者在MySQL中性能几乎没有差别,但COUNT(某字段)会忽略字段值为NULL的行,而COUNT(*)不会。第9题和第28题用COUNT(*),第15题用COUNT(course_id)都能用,但如果你统计的是“有多少人选了课”,而课程ID恰好有NULL,两个结果就不一样了。做题时先问自己:到底想数行数,还是数该字段非NULL的个数。

**第四,LIMIT与分页的性能坑。**第72题查第二名用ORDER BY score DESC LIMIT 1, 1,数据量小没问题,但数据量大时LIMIT偏移量越深越慢。比如LIMIT 100000, 10,数据库要先扫过前10万行再取数据。实际业务中可以用“上一页最后一条记录的ID”来做条件过滤,替代深分页。这也是慢SQL优化的常见场景。

**第五,慢SQL优化别只盯着索引看。**热搜词里“慢sql优化 explain主要看哪些信息”热度很高,我在批改练习时也提醒大家:explain里的type列出现ALL、Extra里出现Using temporary或Using filesort,都要警惕。但优化顺序永远是:先看SQL逻辑是否扫了非必要的数据,再看索引是否生效,最后才考虑改表结构和缓存。很多新人事先给所有字段都加索引,反而导致写操作变慢,这是典型的过度优化。

最后给你一个刷题建议

这套题我自己的使用习惯是:每10题一组,刷完一组就停下来,不看答案重新在空的数据库里自己造一套不同的测试数据,再把这10道题重新写一遍。这样做的目的,是强迫自己理解每一道题背后的语法逻辑和适用场景,而不是把答案背下来。

如果你是在准备面试,我特别建议把第28题、第34题、第49题、第65题、第77题这几道题吃透,因为它们是“查重复、查缺失、查最值、查分组TopN、查排名”五大面试原型的代表,面试官出题基本都是从这些原型变形出来的。把这五类题目背后的写法练到不用想就能写出来,比你盲目刷几百道零散题有用得多。

等100道题全部过完,你可以试着把题目里的“一、二、三班”换成你自己的业务订单表、用户表、商品表,重新套一遍这套练习逻辑。做到那一步,SQL对你来说就不再是背语法,而是真正的工具了。

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

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

立即咨询