我刚学 SQL 那会,背了两周语法,一句"查成绩为空的学生"就把我卡住——我理所当然地写了WHERE score = NULL,结果一行都查不到,因为空值(NULL)根本不能用等号比较。
这篇用学生成绩表和订单表两条主线、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 题:查询所有学生的姓名、课程和成绩
SELECTstudent_name,course_name,scoreFROMstudent_score;只取需要的列,不用SELECT *,避免拖回无关数据。
第 2 题:查询成绩大于等于 90 分的记录
SELECT*FROMstudent_scoreWHEREscore>=90;第 3 题:查询班级为"一班"且成绩在 80 到 90 之间的学生
SELECTstudent_name,course_name,scoreFROMstudent_scoreWHEREclass_name='一班'ANDscoreBETWEEN80AND90;BETWEEN ... AND ...包含两端边界,写成> 80 AND < 90会把 80 和 90 漏掉。
第 4 题:查询课程为"数学"或"英语"的成绩记录
SELECT*FROMstudent_scoreWHEREcourse_nameIN('数学','英语');枚举离散值用IN,比一连串OR更清爽。
第 5 题:查询姓名中包含"张"的学生
SELECT*FROMstudent_scoreWHEREstudent_nameLIKE'%张%';%代表任意多个字符,_代表单个字符。
第 6 题:查询成绩为空的学生记录
SELECT*FROMstudent_scoreWHEREscoreISNULL;错误:<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 题:按成绩从高到低查询
SELECTstudent_name,course_name,scoreFROMstudent_scoreORDERBYscoreDESC;第 8 题:按班级升序、成绩降序排列
SELECTclass_name,student_name,scoreFROMstudent_scoreORDERBYclass_nameASC,scoreDESC;前一个字段值相同,才会轮到后一个字段排序。
第 9 题:查询所有出现过的课程名称并去重
SELECTDISTINCTcourse_nameFROMstudent_score;第 10 题:统计成绩表的总记录数
SELECTCOUNT(*)AStotal_rowsFROMstudent_score;第 11 题:统计数学课程的平均分、最高分、最低分
SELECTAVG(score)ASavg_score,MAX(score)ASmax_score,MIN(score)ASmin_scoreFROMstudent_scoreWHEREcourse_name='数学';COUNT(*)统计全部行数,COUNT(列)只统计该列非空的行数;聚合函数会自动忽略空值。
第 12 题:统计订单表中不同客户的数量
SELECTCOUNT(DISTINCTcustomer_id)AScustomer_countFROMorders;去重后再计数,这正是"独立客户数"的标准写法。
注意:先过滤、再聚合,WHERE 永远跑在聚合函数前面。
四、分组统计:一行 SQL 算出多组结果
全局一个平均分好算,可需求是"每个班、每门课、每个客户"各自的数字,怎么让一条语句同时吐出多组结果?
| 题号 | 业务需求 | 关键写法 |
|---|---|---|
| 13 | 统计每个班的人数 | GROUP BY |
| 14 | 统计每门课的平均分 | 分组 +AVG |
| 15 | 每个班每门课的平均分 | 多字段分组 |
| 16 | 平均分大于 85 的课程 | HAVING |
| 17 | 每个客户的订单总金额 | 分组 +SUM |
| 18 | 总金额超 1000 的客户 | 分组后过滤 |
第 13 题:统计每个班级的学生人数
SELECTclass_name,COUNT(*)ASstudent_countFROMstudent_scoreGROUPBYclass_name;第 14 题:统计每门课程的平均分
SELECTcourse_name,AVG(score)ASavg_scoreFROMstudent_scoreGROUPBYcourse_name;第 15 题:统计每个班级每门课程的平均分
SELECTclass_name,course_name,AVG(score)ASavg_scoreFROMstudent_scoreGROUPBYclass_name,course_name;多字段分组的粒度是字段组合,先按班级、再按课程切分。
第 16 题:查询平均分大于 85 的课程
SELECTcourse_name,AVG(score)ASavg_scoreFROMstudent_scoreGROUPBYcourse_nameHAVINGAVG(score)>85;我在这儿栽过:把聚合条件写进<font style="color:#c00;">WHERE AVG(score) > 85</font>,语句直接报错。
第 17 题:统计每个客户的订单总金额
SELECTcustomer_id,customer_name,SUM(quantity*price)AStotal_amountFROMordersGROUPBYcustomer_id,customer_name;第 18 题:查询订单总金额超过 1000 的客户
SELECTcustomer_id,customer_name,SUM(quantity*price)AStotal_amountFROMordersGROUPBYcustomer_id,customer_nameHAVINGSUM(quantity*price)>1000;行过滤与组过滤到底差在哪?看数据库的逻辑执行顺序:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMITWHERE在分组前逐行过滤,手里根本没有聚合结果;HAVING在分组后执行,专门筛选聚合值。
记住:WHERE 过滤行,HAVING 过滤组,聚合条件只能进 HAVING。
五、连接子查询:跨表拼字段、分步算结果
单表能查的都查完了,可信息散落在多张表,还得回答"谁没下过单""哪单金额最高"这类需要先算一步的问题,这时候怎么办?
| 题号 | 业务需求 | 关键写法 |
|---|---|---|
| 19 | 取学生姓名、课程、成绩、班级 | 单表直查(对比) |
| 20 | 取订单的客户、商品信息 | 单表直查(对比) |
| 21 | 查每个班级的班主任 | JOIN ... ON |
| 22 | 查没下过订单的客户 | NOT IN子查询 |
| 23 | 查金额最高的订单 | 标量子查询 |
| 24 | 查客户总金额并带姓名 | 分组(连接可扩展) |
第 19 题:查询学生姓名、课程、成绩并显示班级
SELECTstudent_name,class_name,course_name,scoreFROMstudent_score;字段都在一张表,直接查即可,不必为了连接而连接。
第 20 题:查询每个订单的客户姓名和商品名称
SELECTorder_id,customer_name,product_name,quantity,priceFROMorders;第 21 题:假设有班级表 class_info(class_name, teacher_name),查每个班的班主任
SELECTs.student_name,s.class_name,c.teacher_nameFROMstudent_score sJOINclass_info cONs.class_name=c.class_name;连接(JOIN)按关联字段拼表,关联条件写在ON之后。我漏写过一次 ON,直接拼出笛卡尔积(Cartesian Product),行数变成两表相乘,测试库当场卡住。
第 22 题:查询没有下过订单的客户
SELECTcustomer_id,customer_nameFROMcustomersWHEREcustomer_idNOTIN(SELECTDISTINCTcustomer_idFROMorders);备注:customers 为客户主表。当子查询结果里混入 NULL 时,<font style="color:#666;">NOT IN</font>会整体失效;更稳的写法是<font style="color:#666;">NOT EXISTS</font>。
SELECTc.customer_id,c.customer_nameFROMcustomers cWHERENOTEXISTS(SELECT1FROMorders oWHEREo.customer_id=c.customer_id);第 23 题:查询订单金额最高的订单信息
SELECT*FROMordersWHEREquantity*price=(SELECTMAX(quantity*price)FROMorders);子查询(Subquery)返回单个值时用=比较;返回多个值要用IN、ANY或ALL。
第 24 题:查询每个客户的订单总金额并列出姓名
SELECTo.customer_id,o.customer_name,SUM(o.quantity*o.price)AStotal_amountFROMorders oGROUPBYo.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 分
UPDATEstudent_scoreSETscore=score+5WHEREcourse_name='数学'ANDscore<60;第 26 题:删除成绩为空的学生记录
DELETEFROMstudent_scoreWHEREscoreISNULL;我见过最疼的事故:<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 题:新建一张学生成绩表
CREATETABLEstudent_score(student_idINT,student_nameVARCHAR(50),class_nameVARCHAR(50),course_nameVARCHAR(50),scoreDECIMAL(5,2),exam_dateDATE);整数用INT、字符串用VARCHAR、小数用DECIMAL、日期用DATE。
第 28 题:向订单表插入一条订单记录
INSERTINTOorders(order_id,customer_id,customer_name,product_name,category,quantity,price,order_date,city)VALUES(1001,1,'张三','笔记本','数码',2,5999.00,'2024-06-01','北京');字段顺序与值顺序必须一一对应。
第 29 题:查询每个城市的订单总金额,并按金额降序
SELECTcity,SUM(quantity*price)AStotal_amountFROMordersGROUPBYcityORDERBYtotal_amountDESC;第 30 题:查询订单总金额排名前 3 的客户
SELECTcustomer_id,customer_name,SUM(quantity*price)AStotal_amountFROMordersGROUPBYcustomer_id,customer_nameORDERBYtotal_amountDESCLIMIT3;备注:<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