主题
SQL 查询:单表、连接与子查询
第三讲,考试的主战场。SELECT 一条语句就能出一道 20 分大题。本讲所有 SQL 都基于第二讲建的 student / course / sc 三张表,可直接运行。
先记住一句话:SELECT 的执行顺序是 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT,别名在 SELECT 里才诞生,所以 WHERE 里不能用 SELECT 起的别名。
一、单表查询
sql
-- 投影 + 选择 + 排序 + 限量,一条语句四种动作
SELECT sno, sname, sage -- ③ 投影:挑列
FROM student -- ① 从哪张表
WHERE sdept = '计算机' AND sage > 18 -- ② 选择:挑行
ORDER BY sage DESC, sno ASC -- ④ 排序
LIMIT 5; -- ⑤ 前 5 条(MySQL 方言)
-- 去重:查询"有哪些系"(投影在数学上自动去重,SQL 必须手动 DISTINCT)
SELECT DISTINCT sdept FROM student;
-- 模糊匹配:LIKE + 通配符(% 任意长串,_ 单个字符)
SELECT sname FROM student WHERE sname LIKE '张%'; -- 姓张的
SELECT sname FROM student WHERE sname LIKE '_明%'; -- 第二个字是"明"WHERE 里的三值逻辑是常见坑:任何与 NULL 的比较结果都是 UNKNOWN,WHERE sage != 18 不会选中 sage 为 NULL 的行。判断空值只能用 IS NULL / IS NOT NULL。
二、聚合与分组:GROUP BY 与 HAVING
五个聚合函数:COUNT SUM AVG MAX MIN。其中 COUNT(*) 数行数,COUNT(列) 不数 NULL。
sql
-- 每个学生的选课门数和平均分
SELECT sno, COUNT(*) AS 门数, AVG(grade) AS 均分
FROM sc
GROUP BY sno;
-- 只看选了 3 门以上的:筛选"组"用 HAVING,不是 WHERE
SELECT sno, COUNT(*) AS 门数, AVG(grade) AS 均分
FROM sc
GROUP BY sno
HAVING COUNT(*) >= 3;🧠 记忆锚点:WHERE 过滤行(分组前),HAVING 过滤组(分组后);WHERE 里不能出现聚合函数。 "查询平均分 80 分以上的学生"必须
HAVING AVG(grade) > 80,写进 WHERE 直接语法错误。
补充两个高频细节:AVG 忽略 NULL 值的行;一个 SQL 里同时出现普通列和聚合函数时,普通列必须出现在 GROUP BY 里。
三、连接查询
考试最爱"每个学生的姓名和平均分"——姓名在 student,分数在 sc,必须连接:
sql
-- 内连接:两边都匹配上才出现在结果里
SELECT s.sname, sc.grade
FROM student s JOIN sc ON s.sno = sc.sno
WHERE sc.cno = 'C01';
-- 左外连接:左边全保留,右边匹配不上补 NULL
-- 经典题"查询没有选任何课的学生":左连后右端为 NULL
SELECT s.sno, s.sname
FROM student s LEFT JOIN sc ON s.sno = sc.sno
WHERE sc.sno IS NULL;三种连接一张表记牢:
| 连接 | 结果集 | 典型用途 |
|---|---|---|
| INNER JOIN | 只保留匹配成功的行 | 常规关联查询 |
| LEFT JOIN | 左表全保留 + 右表补 NULL | "查没有 XX 的"(右端 IS NULL) |
| RIGHT JOIN | 右表全保留 + 左表补 NULL | 少用,可以改写成 LEFT JOIN |
自连接也常考:给 sc 表起两个别名当作两张表用,例如"与 2026001 号同学选了同一门课的学生"。
四、子查询
sql
-- ① 带_IN 的不相关子查询:查选修了"数据库"课程的学生
SELECT sname FROM student
WHERE sno IN (
SELECT sno FROM sc
WHERE cno IN (SELECT cno FROM course WHERE cname = '数据库')
);
-- ② 带比较的子查询:查成绩高于该课平均分(平均分单独算得出一个值)
SELECT sno, grade FROM sc
WHERE cno = 'C01' AND grade > (
SELECT AVG(grade) FROM sc WHERE cno = 'C01'
);
-- ③ 相关子查询 + EXISTS:内层依赖外层的行,逐行判断"存在性"
SELECT sname FROM student s
WHERE EXISTS (
SELECT 1 FROM sc
WHERE sc.sno = s.sno AND sc.cno = 'C01'
);
-- ④ 经典否定题:查没选 C01 课程的学生(NOT EXISTS 双层结构是标准答案写法)
SELECT sname FROM student s
WHERE NOT EXISTS (
SELECT 1 FROM sc WHERE sc.sno = s.sno AND sc.cno = 'C01'
);🧠 记忆锚点:EXISTS 只问"有没有",返回真就留下当前行;"至少选了一门"用 EXISTS,"一门都没选"用 NOT EXISTS,"选了全部课程"用双重 NOT EXISTS(不存在一门课是他没选的)——这个"全称量词转双否定"是本课最著名的题型。
五、动手实验
💡 动手实验 1:插入测试数据后,跑通"每门课的选课人数、最高分、最低分",并把课程名一起显示出来(需要连接 course)。
sqlINSERT INTO student VALUES ('2026001','张三','男',19),('2026002','李四','女',20),('2026003','王五','男',18); INSERT INTO course VALUES ('C01','数据库',3),('C02','操作系统',4); INSERT INTO sc VALUES ('2026001','C01',85),('2026001','C02',78), ('2026002','C01',92),('2026003','C02',66); SELECT c.cname, COUNT(*) AS 人数, MAX(sc.grade) AS 最高, MIN(sc.grade) AS 最低 FROM course c JOIN sc ON c.cno = sc.cno GROUP BY c.cno, c.cname;💡 动手实验 2:把"查询选修了全部课程的学生姓名"写出来(双重 NOT EXISTS),然后故意漏写一层,观察结果差在哪——这是历年考试失分最重的一题。
小结
- 执行顺序 FROM→WHERE→GROUP BY→HAVING→SELECT→ORDER BY→LIMIT;别名诞生于 SELECT
- WHERE 滤行,HAVING 滤组;NULL 只能
IS NULL判断 - LEFT JOIN + IS NULL 是"查不存在"的通用写法;"选了全部"用双重 NOT EXISTS
- 相关子查询逐行求值,不相关子查询只算一次——考试考写法,优化器考成本
下一讲从"查"转到"建":DDL、五类约束与索引——数据定义与索引。