我刚学 SQL 那会,背了两周语法,一句"查成绩为空的学生"就把我卡住——我理所当然地写了 WHERE score = NULL,结果一行都查不到,因为空值(NULL)根本不能用等号比较。
| SQL 练习文件 | 点击查看 |
| 这篇用学生成绩表和订单表两条主线、30 道递进练习,带你把零散语法练成"看懂需求 → 拆开步骤 → 写稳语句"的固定手感。 |

一、练习数据:先把两张表摆上桌
后面所有题目都围绕两张业务表展开,字段先对齐。
学生成绩表 student_score
| student_id | 学号 |
| student_name | 姓名 |
| class_name | 班级 |
| course_name | 课程 |
| score | 成绩 |
| exam_date | 考试日期 |
订单表 orders
| order_id | 订单号 |
| customer_id | 客户编号 |
| customer_name | 客户姓名 |
| product_name | 商品名称 |
| category | 商品类别 |
| quantity | 数量 |
| price | 单价 |
| order_date | 下单日期 |
| city | 城市 |
二、基础查询:先把数据捞准
要分析数据,先得把数据从表里捞出来。可同样是"查",为什么你写的语句要么查多了行,要么漏掉了本该有的记录?
| 1 | 查所有学生的姓名、课程、成绩 | SELECT 投影列 |
| 2 | 查成绩 ≥ 90 的记录 | WHERE + 比较符 |
| 3 | 查一班且成绩在 80–90 | AND + BETWEEN |
| 4 | 查数学或英语课程 | IN |
| 5 | 查姓名中带"张" | LIKE 模糊匹配 |
| 6 | 查成绩为空的记录 | IS NULL |
第 1 题:查询所有学生的姓名、课程和成绩
SELECT student_name, course_name, score
FROM student_score;
只取需要的列,不用 SELECT *,避免拖回无关数据。
第 2 题:查询成绩大于等于 90 分的记录
SELECT *
FROM student_score
WHERE score >= 90;
第 3 题:查询班级为"一班"且成绩在 80 到 90 之间的学生
SELECT student_name, course_name, score
FROM student_score
WHERE class_name = '一班'
AND score BETWEEN 80 AND 90;
BETWEEN … AND … 包含两端边界,写成 > 80 AND < 90 会把 80 和 90 漏掉。
第 4 题:查询课程为"数学"或"英语"的成绩记录
SELECT *
FROM student_score
WHERE course_name IN ('数学', '英语');
枚举离散值用 IN,比一连串 OR 更清爽。
第 5 题:查询姓名中包含"张"的学生
SELECT *
FROM student_score
WHERE student_name LIKE '%张%';
% 代表任意多个字符,_ 代表单个字符。
第 6 题:查询成绩为空的学生记录
SELECT *
FROM student_score
WHERE score IS NULL;
错误:<font style="color:#c00;">WHERE score = NULL</font> 一行都查不到,NULL 与任何值的比较结果都不是"真"。
正确:判空只能用 <font style="color:#080;">IS NULL</font>,非空用 <font style="color:#080;">IS NOT NULL</font>。
三、排序聚合:从逐行记录到一个汇总数字
数据捞出来了,可一堆无序行没法看,需求还想要"数学平均分"“多少个独立客户”。从逐行记录跨到一个汇总数字,这一步怎么写?
| 7 | 成绩从高到低排列 | ORDER BY … DESC |
| 8 | 班级升序、成绩降序 | 多字段排序 |
| 9 | 列出所有出现过的课程 | DISTINCT |
| 10 | 统计总记录数 | COUNT(*) |
| 11 | 数学的平均/最高/最低分 | AVG/MAX/MIN |
| 12 | 统计不同客户的数量 | COUNT(DISTINCT …) |
第 7 题:按成绩从高到低查询
SELECT student_name, course_name, score
FROM student_score
ORDER BY score DESC;
第 8 题:按班级升序、成绩降序排列
SELECT class_name, student_name, score
FROM student_score
ORDER BY class_name ASC, score DESC;
前一个字段值相同,才会轮到后一个字段排序。
第 9 题:查询所有出现过的课程名称并去重
SELECT DISTINCT course_name
FROM student_score;
第 10 题:统计成绩表的总记录数
SELECT COUNT(*) AS total_rows
FROM student_score;
第 11 题:统计数学课程的平均分、最高分、最低分
SELECT AVG(score) AS avg_score,
MAX(score) AS max_score,
MIN(score) AS min_score
FROM student_score
WHERE course_name = '数学';
COUNT(*) 统计全部行数,COUNT(列) 只统计该列非空的行数;聚合函数会自动忽略空值。
第 12 题:统计订单表中不同客户的数量
SELECT COUNT(DISTINCT customer_id) AS customer_count
FROM orders;
去重后再计数,这正是"独立客户数"的标准写法。
注意:先过滤、再聚合,WHERE 永远跑在聚合函数前面。
四、分组统计:一行 SQL 算出多组结果
全局一个平均分好算,可需求是"每个班、每门课、每个客户"各自的数字,怎么让一条语句同时吐出多组结果?
| 13 | 统计每个班的人数 | GROUP BY |
| 14 | 统计每门课的平均分 | 分组 + AVG |
| 15 | 每个班每门课的平均分 | 多字段分组 |
| 16 | 平均分大于 85 的课程 | HAVING |
| 17 | 每个客户的订单总金额 | 分组 + SUM |
| 18 | 总金额超 1000 的客户 | 分组后过滤 |
第 13 题:统计每个班级的学生人数
SELECT class_name, COUNT(*) AS student_count
FROM student_score
GROUP BY class_name;
第 14 题:统计每门课程的平均分
SELECT course_name, AVG(score) AS avg_score
FROM student_score
GROUP BY course_name;
第 15 题:统计每个班级每门课程的平均分
SELECT class_name, course_name, AVG(score) AS avg_score
FROM student_score
GROUP BY class_name, course_name;
多字段分组的粒度是字段组合,先按班级、再按课程切分。
第 16 题:查询平均分大于 85 的课程
SELECT course_name, AVG(score) AS avg_score
FROM student_score
GROUP BY course_name
HAVING AVG(score) > 85;
我在这儿栽过:把聚合条件写进 <font style="color:#c00;">WHERE AVG(score) > 85</font>,语句直接报错。
第 17 题:统计每个客户的订单总金额
SELECT customer_id, customer_name,
SUM(quantity * price) AS total_amount
FROM orders
GROUP BY customer_id, customer_name;
第 18 题:查询订单总金额超过 1000 的客户
SELECT customer_id, customer_name,
SUM(quantity * price) AS total_amount
FROM orders
GROUP BY customer_id, customer_name
HAVING SUM(quantity * price) > 1000;
行过滤与组过滤到底差在哪?看数据库的逻辑执行顺序:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
WHERE 在分组前逐行过滤,手里根本没有聚合结果;HAVING 在分组后执行,专门筛选聚合值。
记住:WHERE 过滤行,HAVING 过滤组,聚合条件只能进 HAVING。
五、连接子查询:跨表拼字段、分步算结果
单表能查的都查完了,可信息散落在多张表,还得回答"谁没下过单""哪单金额最高"这类需要先算一步的问题,这时候怎么办?
| 19 | 取学生姓名、课程、成绩、班级 | 单表直查(对比) |
| 20 | 取订单的客户、商品信息 | 单表直查(对比) |
| 21 | 查每个班级的班主任 | JOIN … ON |
| 22 | 查没下过订单的客户 | NOT IN 子查询 |
| 23 | 查金额最高的订单 | 标量子查询 |
| 24 | 查客户总金额并带姓名 | 分组(连接可扩展) |
第 19 题:查询学生姓名、课程、成绩并显示班级
SELECT student_name, class_name, course_name, score
FROM student_score;
字段都在一张表,直接查即可,不必为了连接而连接。
第 20 题:查询每个订单的客户姓名和商品名称
SELECT order_id, customer_name, product_name, quantity, price
FROM orders;
第 21 题:假设有班级表 class_info(class_name, teacher_name),查每个班的班主任
SELECT s.student_name, s.class_name, c.teacher_name
FROM student_score s
JOIN class_info c
ON s.class_name = c.class_name;
连接(JOIN)按关联字段拼表,关联条件写在 ON 之后。我漏写过一次 ON,直接拼出笛卡尔积(Cartesian Product),行数变成两表相乘,测试库当场卡住。
第 22 题:查询没有下过订单的客户
SELECT customer_id, customer_name
FROM customers
WHERE customer_id NOT IN (
SELECT DISTINCT customer_id
FROM orders
);
备注:customers 为客户主表。当子查询结果里混入 NULL 时,<font style="color:#666;">NOT IN</font> 会整体失效;更稳的写法是 <font style="color:#666;">NOT EXISTS</font>。
SELECT c.customer_id, c.customer_name
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
);
第 23 题:查询订单金额最高的订单信息
SELECT *
FROM orders
WHERE quantity * price = (
SELECT MAX(quantity * price)
FROM orders
);
子查询(Subquery)返回单个值时用 = 比较;返回多个值要用 IN、ANY 或 ALL。
第 24 题:查询每个客户的订单总金额并列出姓名
SELECT o.customer_id,
o.customer_name,
SUM(o.quantity * o.price) AS total_amount
FROM orders o
GROUP BY o.customer_id, o.customer_name;
需要补客户资料时,先按客户聚合,再连接客户表补字段。
注意:连接拼字段,子查询分步算,复杂查询从内层往外写。
六、更新建表:写操作的稳妥落地与 TopN
只读数据不够,你还要改数据、建表、插记录,并给出"消费 Top 3 客户"。写操作一旦出错,后果比查询重得多,怎么稳妥落地?
| 25 | 数学不及格的成绩加 5 分 | UPDATE |
| 26 | 删除成绩为空的记录 | DELETE |
| 27 | 新建学生成绩表 | CREATE TABLE |
| 28 | 插入一条订单记录 | INSERT |
| 29 | 各城市总金额降序 | 分组 + 排序 |
| 30 | 总金额前 3 的客户 | LIMIT 取 TopN |
第 25 题:将数学课程中低于 60 分的成绩统一加 5 分
UPDATE student_score
SET score = score + 5
WHERE course_name = '数学'
AND score < 60;
第 26 题:删除成绩为空的学生记录
DELETE FROM student_score
WHERE score IS NULL;
我见过最疼的事故:<font style="color:#c00;">UPDATE</font>、<font style="color:#c00;">DELETE</font> 漏掉 <font style="color:#c00;">WHERE</font>,整表被改且默认无法撤销。
做法:先把 <font style="color:#080;">WHERE</font> 条件套进 <font style="color:#080;">SELECT</font> 跑一遍,确认命中的行无误,再执行改写。
第 27 题:新建一张学生成绩表
CREATE TABLE student_score (
student_id INT,
student_name VARCHAR(50),
class_name VARCHAR(50),
course_name VARCHAR(50),
score DECIMAL(5,2),
exam_date DATE
);
整数用 INT、字符串用 VARCHAR、小数用 DECIMAL、日期用 DATE。
第 28 题:向订单表插入一条订单记录
INSERT INTO orders (
order_id, customer_id, customer_name,
product_name, category, quantity,
price, order_date, city
) VALUES (
1001, 1, '张三',
'笔记本', '数码', 2,
5999.00, '2024-06-01', '北京'
);
字段顺序与值顺序必须一一对应。
第 29 题:查询每个城市的订单总金额,并按金额降序
SELECT city, SUM(quantity * price) AS total_amount
FROM orders
GROUP BY city
ORDER BY total_amount DESC;
第 30 题:查询订单总金额排名前 3 的客户
SELECT customer_id, customer_name,
SUM(quantity * price) AS total_amount
FROM orders
GROUP BY customer_id, customer_name
ORDER BY total_amount DESC
LIMIT 3;
备注:<font style="color:#666;">LIMIT</font> 为 MySQL/PostgreSQL 写法;SQL Server 用 <font style="color:#666;">TOP</font>,Oracle 用 <font style="color:#666;">FETCH FIRST n ROWS ONLY</font>。
注意:先查后改,UPDATE/DELETE 不带 WHERE 就是事故。
七、总结
30 题走下来,一条主线很清楚:学生成绩表练的是过滤、聚合与分组,订单表练的是业务计算、连接与排序,写操作则逼着你养成"先查后改"的习惯。
想把手感固化,再做三件事:
- 把成绩改成 59、60、61 这类边界值重跑,体会 BETWEEN 与 > 的差异;
- 同一题用 NOT IN、NOT EXISTS、LEFT JOIN 三种写法各写一遍;
- 每条 SQL 都按执行顺序复盘一次,说清每一步的先后。
语法记不住可以随时查,思路错了才会步步错。把这条递进链路练熟,再去碰窗口函数、CTE 和索引优化,会顺很多。

术语速查表
| 空值 | NULL | 未知或缺失值,只能用 IS NULL 判断 |
| 谓词 | Predicate | WHERE 后返回真/假的条件表达式 |
| 聚合函数 | Aggregate Function | 对一组行计算出单个值 |
| 分组 | GROUP BY | 按字段把数据划分为多个组 |
| 连接 | JOIN | 按关联条件拼合多张表 |
| 子查询 | Subquery | 嵌套在另一条语句中的查询 |
| 笛卡尔积 | Cartesian Product | 多表无条件配对,行数相乘 |
| 别名 | Alias | 用 AS 给表或列起的临时名 |
| 分页 | Pagination | 用 LIMIT 分段返回结果 |
参考链接
- MySQL 8.0 Reference Manual — SELECT Statement
- MySQL 8.0 Reference Manual — Aggregate Functions
- MySQL 8.0 Reference Manual — JOIN Syntax
- MySQL 8.0 Reference Manual — UPDATE Syntax
网硕互联帮助中心




评论前必须登录!
注册