☰
MySQL数据查询操作实验避坑指南:从单表条件到多表连接
2026/10/2 19:52:47 网站建设 项目流程

简介:这份PDF文档面向国家开放大学MySQL数据库应用课程的学习者,聚焦实验训练2的数据查询操作,适合正在备考实验、需要系统梳理查询语法的学生与自学者。文档围绕单表查询到复杂嵌套查询逐层展开,涵盖字段查询、多条件查询、DISTINCT去重、ORDER BY排序、GROUP BY分组、COUNT/SUM/AVG/MAX/MIN聚合函数,以及内连接、外连接、复合条件连接和IN、EXISTS嵌套查询等核心知识点,每个实验均配有题目分析与实现思路。资源包共1个PDF文件,约1.75MB,内容紧凑便于打印或离线查阅。目前已有3334人学习下载,可作为实验报告撰写与期末复习的参考材料,帮助读者对照实验编号快速定位语法要点,理解多表连接与子查询的解题逻辑,提升数据库查询语句的编写与调试能力。

1. 数据查询操作:从建表到多表联查,一次实验课到底要跨过几道坎

国家开放大学 MySQL 数据库应用这门课里,实验训练2 的数据查询操作,表面看就是写几条 SELECT,真上手才发现卡点根本不在语法。我带过几届学生的上机课,十个人里有八个会在「查不出结果」和「查出来不对」之间反复横跳——表建好了,数据插进去了,SELECT * 能跑,但一加 WHERE 条件就空集,一加 GROUP BY 就报错,一写多表联查就笛卡尔积爆炸。这篇笔记就按实验训练2 的真实推进顺序,把数据查询操作从单表条件筛选、聚合分组、子查询到多表连接完整走一遍,每一步给出可复现的 SQL、参数说明和翻车现场。适合正在做这个实验的在校生,也适合想系统补一遍 MySQL 查询基础的从业者。你不需要装什么高级工具,mysql 命令行或者 Workbench 都行,关键是每一步都知道自己在查什么、为什么这么查。

2. 实验前的库表准备:先把查询的地基打对

2.1 建库建表与数据类型选择

数据查询操作的前提是有一张结构合理的表。实验训练2 通常会给一套现成的建表语句,但很多人直接复制粘贴就跑,结果字符集不对、字段类型选错,后面查询全是坑。我一般会先确认三件事:字符集用 utf8mb4、主键用自增整数、金额和成绩这类字段用 DECIMAL 而不是 FLOAT。

下面这套建表和插数据的脚本,是我按实验常见场景整理的,包含学生表、课程表和成绩表三张表,后面所有查询都基于它:

-- 建库,字符集用 utf8mb4,避免中文乱码 CREATE DATABASE IF NOT EXISTS lab_query DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE lab_query; -- 学生表:学号为主键,姓名非空,性别用枚举 CREATE TABLE student ( sno CHAR(8) NOT NULL PRIMARY KEY, -- 学号,定长8位 sname VARCHAR(20) NOT NULL, -- 姓名 gender ENUM('男','女') DEFAULT '男', -- 性别 age TINYINT UNSIGNED DEFAULT 18, -- 年龄,无符号 dept VARCHAR(30) -- 院系 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 课程表:课程号为主键 CREATE TABLE course ( cno CHAR(4) NOT NULL PRIMARY KEY, -- 课程号 cname VARCHAR(30) NOT NULL, -- 课程名 credit DECIMAL(3,1) DEFAULT 2.0 -- 学分,一位小数 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 成绩表:学号+课程号联合主键,成绩用 DECIMAL CREATE TABLE score ( sno CHAR(8) NOT NULL, cno CHAR(4) NOT NULL, grade DECIMAL(5,1) DEFAULT 0.0, -- 成绩,保留一位小数 PRIMARY KEY (sno, cno), FOREIGN KEY (sno) REFERENCES student(sno), FOREIGN KEY (cno) REFERENCES course(cno) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 插入测试数据 INSERT INTO student VALUES ('20230101','张伟','男',20,'计算机'), ('20230102','李娜','女',19,'计算机'), ('20230103','王强','男',21,'数学'), ('20230104','赵敏','女',20,'数学'), ('20230105','刘洋','男',22,'物理'); INSERT INTO course VALUES ('C001','数据库原理',3.0), ('C002','数据结构',4.0), ('C003','操作系统',3.5); INSERT INTO score VALUES ('20230101','C001',88.5), ('20230101','C002',76.0), ('20230102','C001',92.0), ('20230102','C003',85.5), ('20230103','C001',60.0), ('20230103','C002',58.5), ('20230104','C002',90.0), ('20230105','C003',72.5);

这段脚本里几个参数值得说清楚。CHAR(8)和VARCHAR(20)的区别在于定长和变长,学号固定 8 位用 CHAR 更省空间,姓名长度不固定用 VARCHAR。DECIMAL(5,1)表示总共 5 位数字、小数点后 1 位,最大能存 9999.9,成绩够用了,千万别用 FLOAT,否则 88.5 可能存成 88.499999。外键约束保证 score 表里的学号和课程号一定在父表里存在,插入脏数据会直接报错,这是好事。

提示:如果你的 MySQL 版本是 8.0 以上,建表时字符集建议显式写 utf8mb4,不要依赖默认值,不同版本默认字符集不一样。

2.2 用 DESC 和 SELECT 确认数据到位

建完表别急着写复杂查询,先用两条命令确认地基没问题:

-- 查看表结构,确认字段类型和约束 DESC student; -- 查看每张表的行数,确认数据插进去了 SELECT 'student' AS tbl, COUNT(*) AS cnt FROM student UNION ALL SELECT 'course', COUNT(*) FROM course UNION ALL SELECT 'score', COUNT(*) FROM score;

DESC会列出字段名、类型、是否可为空、键类型和默认值,重点看主键和外键有没有建上。第二条用 UNION ALL 把三张表的行数拼在一起看,正常应该是 5、3、8。如果行数不对,先别往下做查询,回头检查 INSERT 是不是被外键挡了或者字符集导致中文变问号。这一步花两分钟,能省掉后面半小时的排查。

3. 单表查询:WHERE 条件怎么写才不返回空集

3.1 比较、逻辑与范围条件的组合

单表查询是实验训练2 的重头戏,也是最多人翻车的地方。最常见的现象是:明明表里有数据,加了 WHERE 就返回 Empty set。原因通常有三类——字符串没加引号、NULL 值比较用了等号、条件之间逻辑关系搞反了。

先看一组能直接跑的查询:

-- 查询计算机系年龄大于19岁的学生 SELECT sno, sname, age, dept FROM student WHERE dept = '计算机' AND age > 19; -- 查询年龄在19到21之间的学生(BETWEEN 包含边界) SELECT sno, sname, age FROM student WHERE age BETWEEN 19 AND 21; -- 查询姓张或姓李的学生,LIKE 配合通配符 SELECT sno, sname FROM student WHERE sname LIKE '张%' OR sname LIKE '李%'; -- 查询院系不是计算机的学生,注意 NULL 的处理 SELECT sno, sname, dept FROM student WHERE dept <> '计算机' OR dept IS NULL;

第一条里dept = '计算机'的引号不能省,字符串比较必须带引号,写成dept = 计算机MySQL 会把它当列名去找,直接报 Unknown column。BETWEEN 19 AND 21是闭区间,19 和 21 都能查到,等价于age >= 19 AND age <= 21。LIKE 里%匹配任意多个字符,_匹配一个字符,'张%'就是姓张。最后一条特别要注意,<>不等于比较遇到 NULL 会返回 UNKNOWN,所以必须补OR dept IS NULL,否则院系为空的学生会被漏掉。

注意:任何和 NULL 的比较——= NULL、<> NULL、> NULL——结果都是 UNKNOWN,不会返回任何行。判断空值只能用IS NULL或IS NOT NULL。

3.2 排序、去重与限制返回行数

查出来数据之后,排序和截取是高频操作。ORDER BY 默认升序,DESC 降序,LIMIT 用来限制返回条数,做分页或者只看前几名时特别有用:

-- 按年龄降序排列,年龄相同按学号升序 SELECT sno, sname, age FROM student ORDER BY age DESC, sno ASC; -- 查询所有院系,去掉重复值 SELECT DISTINCT dept FROM student; -- 查询年龄最大的前3名学生 SELECT sno, sname, age FROM student ORDER BY age DESC LIMIT 3; -- 分页:跳过前2条,取接下来2条 SELECT sno, sname FROM student ORDER BY sno LIMIT 2, 2;

ORDER BY age DESC, sno ASC是多列排序,先按年龄降序,年龄一样再按学号升序,这个组合在成绩排名里很常用。DISTINCT作用于后面所有列的组合,SELECT DISTINCT dept是去重院系,如果写SELECT DISTINCT dept, gender就是按院系和性别的组合去重。LIMIT 3取前三条,LIMIT 2, 2是跳过前两条取两条,注意 MySQL 的 LIMIT 偏移量是从 0 开始算的,LIMIT 2, 2返回的是第 3、4 条。

这里有个血泪经验:ORDER BY 和 LIMIT 一起用时,如果排序字段有重复值,每次查询返回的顺序可能不一样,因为 MySQL 不保证稳定排序。做分页时最好在 ORDER BY 里加上主键这种唯一字段,保证结果可复现。

4. 聚合、分组与子查询:统计类查询的写法与边界

4.1 GROUP BY 配合聚合函数的正确姿势

实验训练2 里统计类题目占比很高,比如「查询每个院系的人数」「查询每门课程的平均分」。这类查询的核心是 GROUP BY 加聚合函数,但很多人会踩一个经典坑:SELECT 里出现了非聚合、非分组的列。

-- 统计每个院系的学生人数 SELECT dept, COUNT(*) AS stu_count FROM student GROUP BY dept; -- 统计每门课程的最高分、最低分、平均分 SELECT cno, MAX(grade) AS max_grade, MIN(grade) AS min_grade, ROUND(AVG(grade), 1) AS avg_grade FROM score GROUP BY cno; -- 只统计平均分大于80的课程,用 HAVING 过滤分组 SELECT cno, ROUND(AVG(grade), 1) AS avg_grade FROM score GROUP BY cno HAVING avg_grade > 80; -- 统计每个院系男生和女生的人数,多列分组 SELECT dept, gender, COUNT(*) AS cnt FROM student GROUP BY dept, gender ORDER BY dept, gender;

第一条SELECT dept, COUNT(*)里 dept 出现在 GROUP BY 中,合法。如果你写成SELECT sno, dept, COUNT(*) ... GROUP BY dept,sno 既不是聚合列也不是分组列,MySQL 8.0 默认的 ONLY_FULL_GROUP_BY 模式会直接报错,5.7 以前会随机返回一行,结果不可控。HAVING和WHERE的区别要记牢:WHERE 在分组前过滤行,HAVING 在分组后过滤组,聚合函数的结果只能用 HAVING 过滤。ROUND(AVG(grade), 1)把平均分保留一位小数,不加 ROUND 可能出来一长串。

提示:如果 HAVING 里用了 SELECT 中定义的别名(比如 avg_grade),MySQL 是允许的,但标准 SQL 不允许,换数据库时可能报错,稳妥写法是 HAVING AVG(grade) > 80。

4.2 子查询:IN、EXISTS 与标量子的选择

子查询是实验训练2 的难点,也是慢 SQL 的高发区。常见形式有三种:标量子查询返回一个值、IN 子查询返回一列值、EXISTS 子查询判断是否存在。

-- 标量子查询:查询成绩高于全体平均分的学生 SELECT sno, cno, grade FROM score WHERE grade > (SELECT AVG(grade) FROM score); -- IN 子查询:查询选修了 C001 课程的学生姓名 SELECT sno, sname FROM student WHERE sno IN (SELECT sno FROM score WHERE cno = 'C001'); -- EXISTS 子查询:查询有成绩记录的学生 SELECT sno, sname FROM student s WHERE EXISTS (SELECT 1 FROM score sc WHERE sc.sno = s.sno); -- 相关子查询:查询每门课程中高于该课程平均分的学生 SELECT sno, cno, grade FROM score sc1 WHERE grade > (SELECT AVG(grade) FROM score sc2 WHERE sc2.cno = sc1.cno);

标量子查询必须保证只返回一个值,如果子查询返回多行会报 Subquery returns more than 1 row。IN 子查询适合子查询结果集不大的场景,结果集超过几千行时性能会明显下降。EXISTS 只关心子查询有没有返回行,不关心返回什么,所以写SELECT 1就够了,它比 IN 更适合大表关联判断。最后一条相关子查询,子查询里引用了外层的sc1.cno,每处理一行外层数据就执行一次子查询,逻辑清晰但数据量大时慢,实际项目里更推荐用 JOIN 改写。

这里给一个用 JOIN 改写相关子查询的例子,结果一样但通常更快:

-- 用 JOIN 改写上面的相关子查询 SELECT sc1.sno, sc1.cno, sc1.grade FROM score sc1 JOIN (SELECT cno, AVG(grade) AS avg_g FROM score GROUP BY cno) t ON sc1.cno = t.cno WHERE sc1.grade > t.avg_g;

子查询先算出每门课的平均分,再和原表 JOIN,避免了逐行执行子查询。两种写法都要会,考试可能考子查询,实际工作更倾向 JOIN。

5. 多表连接查询:JOIN 类型选错,结果全废

5.1 INNER JOIN、LEFT JOIN 与 RIGHT JOIN 的差异

多表连接是实验训练2 最后一块硬骨头。三种连接的差别用一句话概括:INNER JOIN 只返回两表都匹配的行,LEFT JOIN 返回左表全部加右表匹配的,RIGHT JOIN 反过来。选错了不是报错,而是结果少行或多行,最难排查。

-- INNER JOIN:只查有成绩的学生和课程信息 SELECT s.sno, s.sname, c.cname, sc.grade FROM student s INNER JOIN score sc ON s.sno = sc.sno INNER JOIN course c ON sc.cno = c.cno; -- LEFT JOIN:查所有学生,没成绩的也显示,成绩为 NULL SELECT s.sno, s.sname, sc.cno, sc.grade FROM student s LEFT JOIN score sc ON s.sno = sc.sno; -- 找出没选任何课程的学生,用 LEFT JOIN + IS NULL SELECT s.sno, s.sname FROM student s LEFT JOIN score sc ON s.sno = sc.sno WHERE sc.sno IS NULL;

第一条 INNER JOIN 链式连接三张表,只返回有成绩记录的学生,张伟、李娜这些有成绩的会出现,如果某个学生没选课就不会出现在结果里。第二条 LEFT JOIN 以 student 为左表,所有学生都保留,没成绩的课程号和成绩显示 NULL。第三条是 LEFT JOIN 的经典用法——找左表有而右表没有的记录,WHERE sc.sno IS NULL筛出没选课的学生。这个模式在实际工作里叫反连接,比 NOT IN 更安全,因为 NOT IN 遇到子查询里有 NULL 会返回空集。

注意:LEFT JOIN 后面的 WHERE 条件如果写在 ON 里和写在 WHERE 里效果不同。ON sc.cno = 'C001'会在连接时就过滤,左表所有行保留;WHERE sc.cno = 'C001'会在连接后过滤,把左表没匹配的行也删掉,LEFT JOIN 就退化成 INNER JOIN 了。

5.2 多表连接的执行顺序与笛卡尔积防范

三张表以上连接时,写 JOIN 的顺序和 ON 条件的位置会影响结果和性能。最常见的翻车是漏写 ON 条件,导致笛卡尔积——两张表行数相乘,5 个学生乘 8 条成绩出来 40 行,数据量一大直接卡死。

-- 错误示范:漏写 ON 条件,产生笛卡尔积 SELECT s.sname, c.cname FROM student s, course c; -- 5 * 3 = 15 行,全是无意义组合 -- 正确写法:显式 JOIN + ON SELECT s.sname, c.cname, sc.grade FROM student s JOIN score sc ON s.sno = sc.sno JOIN course c ON sc.cno = c.cno WHERE s.dept = '计算机' ORDER BY s.sno, c.cno;

第一条用逗号连接但没写 WHERE 关联条件,MySQL 会返回所有组合,这就是笛卡尔积。第二条显式写 JOIN 和 ON,连接条件清晰,再加 WHERE 过滤院系。写多表查询时我一般遵循三个习惯:一是永远用显式 JOIN 语法,不用逗号加 WHERE;二是每写一个 JOIN 立刻补上 ON;三是连接字段尽量都有索引,外键字段 MySQL 会自动建索引,但非外键的连接字段要手动加。

连接字段的数据类型也要一致。如果 student.sno 是 CHAR(8),score.sno 是 VARCHAR(8),虽然值一样,但 MySQL 可能无法用索引,导致全表扫描。建表时关联字段类型统一,这是建表阶段就该定好的事。

6. 查询排查避坑:五条上机课反复出现的翻车记录

6.1 现象:WHERE 条件查不出数据,表里明明有

原因:字符串值没加引号,或者和 NULL 做了等值比较。WHERE sname = 张伟会被当成列名,WHERE dept = NULL永远返回空集。

解决:字符串一律加单引号,判断空值用IS NULL。排查时先把 WHERE 去掉看全表有没有数据,再逐条加条件定位是哪一条出的问题。

6.2 现象:GROUP BY 查询报错 ONLY_FULL_GROUP_BY

原因:SELECT 列表里出现了既不在 GROUP BY 中、也没被聚合函数包裹的列。MySQL 8.0 默认开启 ONLY_FULL_GROUP_BY,5.7 之前不报错但结果随机。

解决:要么把该列加进 GROUP BY,要么用聚合函数包起来。比如SELECT dept, sname, COUNT(*) ... GROUP BY dept就是错的,sname 要么去掉,要么改成GROUP_CONCAT(sname)。

6.3 现象:多表连接结果行数比预期多很多

原因:漏写 ON 条件产生笛卡尔积,或者连接条件不唯一导致一对多放大。比如一个学生有多条成绩,和课程表连接时每个成绩都会匹配一次课程。

解决:先单独查每张表的行数,再查连接后的行数,对比是否合理。检查每个 JOIN 是否都有 ON,ON 的字段是否能唯一确定匹配关系。

6.4 现象:LEFT JOIN 查出来和 INNER JOIN 一样

原因:WHERE 子句里对右表字段加了非空过滤条件,把左表没匹配的行过滤掉了,LEFT JOIN 退化成 INNER JOIN。

解决:把右表的过滤条件从 WHERE 移到 ON 里。比如LEFT JOIN score sc ON s.sno = sc.sno WHERE sc.grade > 80会丢掉没成绩的学生,改成LEFT JOIN score sc ON s.sno = sc.sno AND sc.grade > 80才能保留。

6.5 现象:子查询报 Subquery returns more than 1 row

原因:标量子查询的位置(比如WHERE grade > (SELECT ...))要求子查询只返回一个值,但子查询实际返回了多行。

解决:确认子查询是否应该加聚合函数(AVG、MAX 等)保证单值,或者改用 IN、EXISTS、JOIN 来处理多行结果。用SELECT COUNT(*)先看子查询返回几行,定位问题最快。

7. 用 EXPLAIN 看懂查询计划:一条慢 SQL 的排查习惯

实验训练2 的查询数据量小,怎么写都快,但养成看执行计划的习惯,到了真实项目能救命。EXPLAIN 放在 SELECT 前面,MySQL 会告诉你这条查询走了哪个索引、扫描了多少行、有没有用临时表。

-- 查看单表条件查询的执行计划 EXPLAIN SELECT sno, sname FROM student WHERE dept = '计算机'; -- 查看多表连接的执行计划 EXPLAIN SELECT s.sname, c.cname, sc.grade FROM student s JOIN score sc ON s.sno = sc.sno JOIN course c ON sc.cno = c.cno WHERE s.dept = '计算机';

输出里重点看四列。type 列从好到坏是 const、eq_ref、ref、range、index、ALL,出现 ALL 就是全表扫描,数据量大时要优化。key 列显示实际用了哪个索引,如果是 NULL 说明没走索引。rows 列是预估扫描行数,越小越好。Extra 列出现 Using filesort 或 Using temporary 说明有额外排序或临时表开销,也值得关注。

我一般排查慢查询的顺序是:先 EXPLAIN 看 type 和 key,确认有没有走索引;再看 rows 估算是否合理;最后看 Extra 有没有 filesort。如果 WHERE 或 JOIN 的字段没索引,加索引通常是最直接的优化。但索引不是越多越好,每个索引都会拖慢写入,实验阶段先理解「连接字段和常用过滤字段加索引」这个原则就够了。

最后说个我自己的习惯:每次写完一条复杂查询,先不加 ORDER BY 和 LIMIT 跑一遍看总行数,确认结果集大小符合预期,再加排序和分页。这个顺序能帮你快速区分是连接逻辑错了还是排序截取错了。数据查询操作这件事,语法只是入场券,真正拉开差距的是对结果集的预判和验证习惯。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询