
我到现在还记得第一次带这个实验时有个学生跑过来问我“老师SELECT * FROM student 不是已经能查了吗为什么实验3还要写这么多查询”我当时没直接回答让他把实验要求里的题目一条条做下去做到第4题他就沉默了——“查询每个系部中年龄大于20岁的学生人数按人数降序输出”。SELECT * 明显搞不定。这就是“实验3 通过SQL进行表查询”的核心不是让你学会敲一条SELECT而是让你能把一句人话描述的需求翻译成结构正确、结果准确的SQL语句。这篇内容是我这么多年带数据库实验、也在实际项目里写SQL攒下来的经验整理适合正在做这个实验的学生、准备数据库考试的人以及刚入职写SQL还不怎么顺的实习生。实验本身不复杂但它是后面所有查询能力的地基——子查询、连接、报表统计、数据清洗全都建立在这几次课的内容上。1. 这个实验究竟在考察什么把“查询”拆开看1.1 表查询不只是一条SELECT很多初学者以为表查询就是背几个语句格式实际上实验3考察的是三层能力。第一层是能读懂需求。实验题通常是中文描述比如“查询年龄在20到22岁之间、且来自计算机系的学生”你得能抓住过滤条件有哪些、有没有隐含的排序或去重要求。第二层是把需求映射为SQL子句。这一步是核心也是多数人卡住的地方。你得知道“条件筛选”对应WHERE“按XX分组统计”对应GROUP BY“分组后再筛”对应HAVING“拼接多张表”对应JOIN或子查询。不是背出来的是练出来的。第三层是验证结果是否正确。做完一条查询要能自己判断结果对不对。这个能力最容易被忽略也最能在实验报告里拉分。我见过不少同学语句能跑通但结果一看就是错的——比如多算了几行、平均数没算上空值、连接之后行数翻倍。他自己不知道老师一眼就看出问题。所以做实验3不要只追求“SELECT能用”要追求“每个题目都知道为什么这么写”。1.2 实验环境选型SQL Server还是MySQL这个实验在不同学校用的数据库平台不太一样常见的是SQL Server和MySQL少数用Oracle或PostgreSQL。根据热搜词也能看出来SQL Server和MySQL是两大主力。我的建议是别纠结选哪个跟着你课程走重点是写标准SQL。因为实验3涉及的单表查询、聚合、连接、子查询90%的语法在各大数据库里是通用的。差异主要在几个地方功能点SQL ServerMySQL限制返回行数TOP nLIMIT n字符串拼接CONCAT()空值处理ISNULL()IFNULL() / COALESCE()分页写法OFFSET...FETCHLIMIT...OFFSET自增列IDENTITYAUTO_INCREMENT如果你用的是SQL Server 2008 R2或2019、2022实验里一般会配SQL Server Management StudioSSMS。如果是MySQL常见配MySQL Workbench或者DBeaver。这几个工具都不影响你的SQL本身最多是快捷键和界面不一样。注意同样是SQL Server2008 R2和2019的兼容性略有差异但实验3用到的语法基本没有区别。如果遇到“无法启动Windows Management Instrumentation服务”这类安装报错多半是系统服务问题跟SQL语法无关先解决环境再写查询。1.3 先把实验数据准备好做查询实验一定要有数据不然所有语句都是空对空。我建议不要只依赖老师给的数据自己动手建一套经典的学生-课程-选课三张表。这套表结构几乎是数据库课程的标配后面学更新、删除、视图、存储过程还能继续用。建表语句如下兼容MySQL和大多数标准SQL环境-- 学生表 CREATE TABLE student ( sno CHAR(9) PRIMARY KEY, -- 学号 sname VARCHAR(20) NOT NULL, -- 姓名 ssex CHAR(2), -- 性别 sage INT, -- 年龄 sdept VARCHAR(20) -- 系部 ); -- 课程表 CREATE TABLE course ( cno CHAR(4) PRIMARY KEY, -- 课程号 cname VARCHAR(40) NOT NULL, -- 课程名 cpno CHAR(4), -- 先修课程号可为空 credit INT -- 学分 ); -- 选课表 CREATE TABLE sc ( sno CHAR(9), cno CHAR(4), grade DECIMAL(5,2), -- 成绩允许为空表示缺考 PRIMARY KEY (sno, cno) );插入一些不复杂但覆盖各种情况的数据INSERT INTO student VALUES (2018001, 张伟, 男, 19, CS), (2018002, 李丽, 女, 20, CS), (2018003, 王强, 男, 21, IS), (2018004, 赵敏, 女, 20, MA), (2018005, 陈晨, 男, 22, CS); INSERT INTO course VALUES (C001, 数据库原理, NULL, 4), (C002, 数据结构, C001, 4), (C003, 操作系统, C002, 3), (C004, 计算机网络, C002, 3); INSERT INTO sc VALUES (2018001, C001, 85), (2018001, C002, 78), (2018002, C001, 92), (2018002, C003, NULL), (2018003, C001, 65);注意我故意放了几个伏笔有一个学生没选任何课有一门选课成绩是NULL课程表里有先修课程字段。这些在后面练习多表查询和空值处理时都是现成的题目素材。2. 从SELECT到WHERE单表查询的完整展开2.1 先记住SQL的“真实执行顺序”这是我在实验课上反复强调的一点SQL的书写顺序和逻辑执行顺序不是一回事。书写顺序是SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ...但数据库实际执行的逻辑顺序大致是FROM先确定从哪张表取数据WHERE对每一行做条件筛选GROUP BY把筛选后的行分组HAVING对分组后的组做筛选SELECT投影需要的列计算表达式ORDER BY对结果排序LIMIT/TOP截取部分行这个顺序解释了三个初学必踩的坑。第一WHERE里不能用SELECT中定义的别名因为WHERE执行在SELECT投影之前。第二HAVING可以用聚合函数条件WHERE不可以。第三ORDER BY通常可以使用别名或未在SELECT中出现的列因为排序发生在投影之后但不同数据库有差异别拿一个库的规则硬套另一个。我的建议是每次写完一条查询心里默默走一遍这个顺序能少犯很多逻辑错误。2.2 列筛选、别名和计算列单表查询的入门操作是选列。实验里常见的写法-- 查询全部列 SELECT * FROM student; -- 查询指定列 SELECT sno, sname, sdept FROM student;但比较容易被忽视的是计算列和别名。比如查询学生出生年份用当前年份减年龄SELECT sno, sname, 2026 - sage AS birth_year FROM student;这里的2026 - sage就是计算列AS birth_year给计算结果起了个别名。我现在写SQL的习惯是只要用了函数或表达式一定要起别名否则结果集的列名会很难看后续程序里取数据也会麻烦。还有CASE WHEN这是表达式不是查询语句但实验里偶尔会用到SELECT sno, sname, CASE WHEN sage 20 THEN 低龄 WHEN sage BETWEEN 20 AND 22 THEN 正常 ELSE 高龄 END AS age_group FROM student;CASE WHEN本质上是“在查询过程中做条件判断并生成新值”它能解决很多看似复杂的题目比如把成绩转成等级。建议在实验3就把它练熟后面做报表会非常有用。2.3 WHERE条件比较、范围、集合与模糊匹配WHERE是单表查询的重头戏实验题一半以上都在考这个。常见条件有以下几类。比较运算,,,,,!或。-- 查询年龄大于20的学生 SELECT sno, sname FROM student WHERE sage 20;范围判断BETWEEN ... AND ...注意是闭区间包含边界值。-- 查询年龄在20到22之间 SELECT * FROM student WHERE sage BETWEEN 20 AND 22;集合判断IN和NOT IN。-- 查询CS和IS两个系的学生 SELECT * FROM student WHERE sdept IN (CS, IS);模糊匹配LIKE和通配符%任意多字符和_单个字符。-- 查询姓张的学生 SELECT * FROM student WHERE sname LIKE 张%; -- 查询名字第二个字是“丽”的学生 SELECT * FROM student WHERE sname LIKE _丽%;这里有个小细节我要提醒当年我做实验时用_丽%去匹配“李丽”这类名字结果有时候匹配不准。原因在于中文字符和数据库字符集、排序规则有关。在SQL Server里如果排序规则设置不当一个中文可能被当成多个字节_可能匹配不到预期位置。遇到这种情况别慌先确认你的数据库字符集是utf8或utf8mb4再检查排序规则。实践中最稳妥的方式是先用%测试确认能匹配后再缩小范围。另外模糊查询的通配符需要转义。比如查询包含%字符的字符串必须写ESCAPESELECT * FROM some_table WHERE some_col LIKE %\%% ESCAPE \\;这个语法冷门但实验报告里出现过一次掌握后印象分能加不少。2.4 DISTINCT、ORDER BY和NULL的特殊性去重用DISTINCT注意是对后面整个列组合去重不是只对第一列去重-- 查询有哪些系部 SELECT DISTINCT sdept FROM student;排序用ORDER BY默认升序降序要加DESC-- 按年龄降序同年龄按学号升序 SELECT sno, sname, sage FROM student ORDER BY sage DESC, sno ASC;NULL值在SQL里是最容易出错的地方。很多人会直觉地写sage NULL或grade NULL这是错误的NULL不能与任何值比较判断空值必须用IS NULL或IS NOT NULL。-- 查询缺考的学生成绩为空 SELECT sno, cno FROM sc WHERE grade IS NULL;为什么因为NULL在SQL里表示“未知”grade NULL的结果不是TRUE也不是FALSE而是UNKNOWNWHERE只会保留结果为TRUE的行。这个特性也影响聚合函数、外连接和子查询后面会反复遇到。3. 聚合与分组从“查出来”到“算出来”3.1 五个聚合函数以及COUNT的两个常见误区聚合函数就是把多行计算成一行。常用的五个COUNT计数、SUM求和、AVG求平均、MAX求最大、MIN求最小。-- 学生总人数 SELECT COUNT(*) FROM student; -- 各科最高分、最低分、平均分 SELECT cno, MAX(grade) AS max_grade, MIN(grade) AS min_grade, AVG(grade) AS avg_grade FROM sc GROUP BY cno;COUNT有两个常见的坑。第一COUNT(*)和COUNT(列名)不同。COUNT(*)统计所有行包括某列为NULL的行COUNT(grade)只统计grade不为NULL的行。比如选课表里有4条记录但其中一条grade是NULLCOUNT(*)返回4COUNT(grade)返回3。第二COUNT(DISTINCT 列)可以统计去重后的数量比如统计有多少个学生选了课SELECT COUNT(DISTINCT sno) FROM sc;还有一点要记牢SUM、AVG、MAX、MIN都会自动忽略NULL但AVG忽略NULL不是把NULL当0而是直接不参与计算。比如两条成绩85和NULLAVG结果是85不是42.5。这个细节在实验报告里很容易被老师当提问点。3.2 GROUP BY的分组逻辑和黄金原则GROUP BY的直观理解是把相同值的行归为一组。举个生活的例子把一筐水果按颜色分类再数每堆有几个这就是GROUP BY 颜色COUNT(*)。写分组查询有一条黄金原则SELECT列表里只能出现分组列和聚合函数。-- 正确按系部分组统计人数 SELECT sdept, COUNT(*) AS cnt FROM student GROUP BY sdept; -- 错误sname既不在GROUP BY里也没被聚合 SELECT sdept, sname, COUNT(*) FROM student GROUP BY sdept;但这个原则在不同数据库的严格程度不一样。MySQL在没有开启ONLY_FULL_GROUP_BY模式时上面那条错误SQL也能跑通返回哪个sname不确定。这种语法宽容反而害人因为结果看起来正常实际是随机的。我在实验课上要求学生一律按标准SQL写宁可报错也不能蒙对。多列分组也很常用比如按系部和性别统计SELECT sdept, ssex, COUNT(*) AS cnt FROM student GROUP BY sdept, ssex;这个分组逻辑就是先按系部分再在系内按性别分。3.3 HAVING与WHERE的分工WHERE是在分组前过滤行HAVING是在分组后过滤组。这个区别在做统计题时至关重要。举个例子题目是“查询平均成绩大于80分的课程”。你应该先按课程分组计算出每门课的平均分再过滤掉平均分小于等于80的组。这是典型的HAVING场景SELECT cno, AVG(grade) AS avg_grade FROM sc GROUP BY cno HAVING AVG(grade) 80;如果写成WHERE AVG(grade) 80数据库会直接报错因为WHERE执行在分组之前那时还没有平均分这个概念。反过来如果题目是“查询成绩大于80分的选课记录”那就是行级过滤用WHERESELECT * FROM sc WHERE grade 80;一句话记忆WHERE管行HAVING管组。3.4 分组查询的常见扣分点我在批改实验报告时分组题的扣分点集中在三处。第一SELECT里混入了非分组列结果是随机值这在MySQL宽松模式下尤其隐蔽。第二把可以在WHERE里过滤的行条件写进了HAVING。虽然结果可能一样但效率差——先分组再过滤比先过滤再分组要慢数据量大时会非常明显。第三忘了处理GROUP BY后与NULL相关的问题。比如按sdept分组如果某行的sdept是NULL会被单独分成一组COUNT结果可能出乎意料。4. 多表查询JOIN连接与子查询的实战套路4.1 为什么要连接连接的本质是什么前面建了三张表如果把所有信息塞进一张大表比如把学生姓名、课程名、成绩都放一起会出现大量重复数据。所以正规做法是拆成多张表用学号、课程号这类键来关联。查询时再把它们按关联条件拼回去这个“拼回去”的动作就是连接。连接的本质可以理解成把左边表的每一行按连接条件去右边表找匹配的行找到就拼成一行输出。找不到就看连接类型决定是否保留左表或右表的数据。4.2 INNER JOIN把关联数据拼起来查询每个学生的选课信息和成绩需要三张表连接SELECT student.sno, student.sname, course.cname, sc.grade FROM student JOIN sc ON student.sno sc.sno JOIN course ON course.cno sc.cno ORDER BY student.sno;这里我显式写了student.sno、course.cname是因为多张表中可能存在同名列比如三张表都有cno或sno不写表名前缀会报错或产生歧义。连接条件一定写在ON后面。如果连接条件漏了结果就是笛卡尔积——两表行数相乘数据会爆炸。实验中最常见的就是这种漏写我第5部分会展开讲。4.3 LEFT JOIN保留左表是查询题的经典考点LEFT JOIN会保留左表的全部行右表没有匹配时右表列显示NULL。这个特性非常适合回答“哪些学生没有选课”这类问题。-- 查询没选任何课程的学生 SELECT student.sname FROM student LEFT JOIN sc ON student.sno sc.sno WHERE sc.cno IS NULL;逻辑是LEFT JOIN先把所有学生和选课记录拼在一起没选课的学生对应的sc.cno是NULL再通过WHERE过滤出这些行。我在实验课上常提醒一个易错点一旦用了LEFT JOIN如果过滤条件写得不对LEFT JOIN会退化成INNER JOIN。比如上面如果把WHERE sc.cno IS NULL换成WHERE sc.grade 60因为NULL不满足条件没选课的学生会被过滤掉左表保不住。很多人被这个现象坑过。4.4 子查询IN、EXISTS与“全部”语义子查询是嵌套在查询里的查询处理多表问题时是另一套思路。用IN子查询可以查“选修了C001课程的学生”SELECT sname FROM student WHERE sno IN (SELECT sno FROM sc WHERE cno C001);用EXISTS和相关子查询可以表达IN不方便表达的场景。比如“查询没有选任何课程的学生”SELECT sname FROM student s WHERE NOT EXISTS ( SELECT 1 FROM sc WHERE sc.sno s.sno );注意我用了SELECT 1而不是SELECT *这是习惯EXISTS只关心子查询是否有返回行不关心具体值写1更清晰。“全部”类语义的题目是实验的分水岭比如“查询所有课程成绩都大于60分的学生”。翻译成人话就是“不存在任何一门课程成绩小于等于60分”。用NOT EXISTS很容易表达SELECT sname FROM student s WHERE NOT EXISTS ( SELECT 1 FROM sc WHERE sc.sno s.sno AND sc.grade 60 );这种“没有反例”的思维在数据库里特别重要我建议在实验3就建立起来。4.5 两种连接写法的差异老式写法是在FROM里写多张表在WHERE里写连接条件SELECT student.sname, sc.grade FROM student, sc WHERE student.sno sc.sno;新式写法用JOIN...ON功能上两者等价。但我的建议是用JOIN...ON有几个原因第一连接条件和过滤条件分离可读性好第二外连接LEFT JOIN等只能用ON表达连接条件WHERE写法在复杂外连接场景下容易出错第三现代数据库查询优化器对新写法支持更成熟更容易看执行计划。5. 实验翻车现场从现象到根因的排查链路这一部分是我最想写的。因为实验课的真实情况是学生不是被SQL语法难倒而是被各种“看起来合理但结果错误”的问题反复折磨。5.1 中文乱码字符集三层不一致现象查询结果里中文全部变成“?”或乱码。排查链路先看客户端显示工具是否设置正确再看连接字符串是否指定字符集最后看数据库表本身的字符集。三个环节任何一层不一致中文都会出问题。MySQL建库建表建议显式指定CREATE DATABASE school DEFAULT CHARACTER SET utf8mb4;SQL Server则要注意排序规则Collation中文一般选Chinese_PRC_CI_AS。这个坑跟SQL查询本身无关但十个实验里有三个会栽在这上面。5.2 分组报错ONLY_FULL_GROUP_BY在揪你现象执行分组查询MySQL报错“Expression #2 of SELECT list is not in GROUP BY clause”。根因MySQL 5.7以上默认开启了ONLY_FULL_GROUP_BY要求SELECT里的非聚合列必须出现在GROUP BY中。解决要么规范改写SELECT sdept, COUNT(*) FROM student GROUP BY sdept;要么临时关闭模式SET sql_mode ;但实验课我不建议关闭严格要求自己按标准写后面到实际项目里能少踩很多坑。5.3 JOIN后行数爆炸连接条件丢了现象一条语句跑半天返回结果上万行一看明显不对。排查链路先单独查两表的行数再查连接后行数。如果连接后行数等于两表行数乘积基本可以确定是笛卡尔积。检查ON条件是不是漏了或者写到了WHERE里但条件恒真。再有一种情况是连接列不唯一比如一对多连接导致行数膨胀这时需要思考业务本身是不是需要这样做。5.4 NULL参与运算结果全空现象执行SELECT grade * 1.1 FROM sc结果里大量NULL。根因SQL规定任何值与NULL运算结果都是NULL。解决根据业务需求用COALESCE或IFNULL把NULL替换成合适的值SELECT sno, cno, COALESCE(grade * 1.1, 0) AS new_grade FROM sc;但要不要把NULL当成0要看场景。统计平均分时NULL通常应该忽略计算工资总额时缺失值可能需要特殊处理。这也是实验报告老师最爱追问的点。5.5 排序结果不稳定少了ORDER BY现象没有加ORDER BY同一查询两次执行结果顺序不一样。这是正常现象。关系数据库理论里表的行没有固定顺序不指定ORDER BY时数据库可能根据查询计划返回任意顺序。千万不要依赖“默认顺序”。另外中文排序在不同排序规则下结果不同想要稳定结果要么显式指定排序规则要么用拼音排序函数MySQL里可用CONVERT(列 USING gbk)。5.6 顺带说一句SQL注入的事实际开发中千万不要通过字符串拼接用户输入来构造SQL语句比如把用户输入的姓名直接拼进WHERE sname ...。实验课虽然只学查询语法但这个安全意识要提前建立。正确方式是使用参数化查询编程语言的驱动和ORM都会支持。这条不属于实验内容但如果你以后写项目越早知道越好。6. 实验之外几个能拉开差距的小习惯6.1 用执行计划叫醒你的SQL写完查询不是结束还可以看看数据库是怎么执行这条语句的。MySQL里在查询前加EXPLAINEXPLAIN SELECT * FROM sc WHERE sno 2018001;SQL Server里可以用“显示估计的执行计划”。看执行计划主要关注有没有全表扫描、有没有用到索引。可能一开始看不明白但养成这个习惯后你比同龄人早一步理解慢SQL优化。6.2 格式化SQL与命名规范实验报告里的SQL如果又长又乱老师批改时印象会差很多。我的建议是每个子句单独一行关键字统一大写表和列名统一小写别名清晰。例如SELECT s.sno, s.sname, COUNT(*) AS course_count FROM student s JOIN sc ON s.sno sc.sno GROUP BY s.sno, s.sname HAVING COUNT(*) 2 ORDER BY course_count DESC;这条语句用了表别名s看起来清爽逻辑也清楚。给自己看、给别人看都省心。6.3 索引意识慢查询的第一反应很多同学学完查询就完了不知道还有索引这回事。简单说WHERE条件和JOIN条件里经常用来筛选、连接的那些列加上索引后查询速度能提升几个数量级。实验数据量小感觉不到差异但参加工作后一张表几百万行没索引的查询可能要几十秒加个索引毫秒级返回。实验3阶段不需要深入调优只要知道“查询慢时先看索引”这个方向即可。6.4 实验报告整理结果的小技巧提交实验报告时除了写清每个题目的SQL语句和运行结果截图我建议再附一列“结果说明”用一两句话描述每条查询解决的问题。比如“本查询通过LEFT JOIN和IS NULL找出从未选课的学生”这不仅让老师看懂你的思路也能帮你自己复习。另外把运行结果里的关键数据记下来哪怕没有截图也能检验自己是否理解正确。带过几轮实验后我发现那些能在实验3里把每个查询题目都写成两种以上写法的学生后面学到存储过程、报表开发、数据清洗时普遍更顺。SQL这个东西看起来是语法问题其实是逻辑问题。你在实验3练的每一句WHERE、每一次JOIN本质上都是在练“如何把一个模糊的问题定义清楚”。如果条件允许建议你把实验的每个查询都手动敲一遍不要复制也不要只在脑海里过。敲错几次报错信息比看十遍课件都管用。