数据库册 · 配套教学 · 按《数据库系统概论》第 6 版 SQL 章顺序

SQL 语法教学

从"为什么这样写"讲到"怎么写",共八课。每课结构:概念讲解 → 可运行示例 → 要点提示 → 课堂练习(可展开看参考答案)。 全部示例使用教材中的"学生—课程"数据库:Student(Sno, Sname, Ssex, Sage, Sdept)、Course(Cno, Cname, Cpno, Ccredit)、SC(Sno, Cno, Grade)。

8 课 · 含练习 讲解 + 示例 教材同序

第 1 课 语句骨架与执行顺序

SQL 查询语句执行顺序流程图
SQL 的执行顺序:FROM → ON → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT(书写顺序 ≠ 执行顺序)

先理解 SQL 是"描述结果"的语言,再记住骨架与执行顺序。

SQL 是非过程化语言:只说明"要什么数据",不说明"怎么取"。 一条查询语句由若干子句组成,书写顺序与执行顺序不同——这是初学时最容易混淆的地方。 数据库先根据 FROM 确定数据来源,用 WHERE 过滤行,再按 GROUP BY 分组、HAVING 过滤组, 最后才计算 SELECT 中的表达式,并用 ORDER BY 排序。

SELECT [ALL|DISTINCT] <目标列表达式>[, ...]
FROM <表名或视图名>[, ...]
[WHERE <行条件>]
[GROUP BY <列名> [HAVING <组条件>]]
[ORDER BY <列名> [ASC|DESC]];

-- 执行顺序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
-- 因此:WHERE 中不能出现聚集函数,SELECT 中的列别名也不能在 WHERE 中使用
要点
  • WHERE 作用于分组前的行,HAVING 作用于分组后的组。
  • ORDER BY 永远最后执行,所以只有它能使用 SELECT 中定义的别名。
  • DISTINCT 作用于整个结果行,不是单个列。
课堂练习(3 题,点击展开参考答案)
  1. 说明 SELECT AVG(Grade) FROM SC WHERE AVG(Grade) > 80; 为什么报错,该怎么改? 答:WHERE 中不允许使用聚集函数。本例不需要分组,直接写 SELECT AVG(Grade) FROM SC;;若按课程统计则应写 SELECT Cno, AVG(Grade) FROM SC GROUP BY Cno HAVING AVG(Grade) > 80;
  2. SELECT Sname AS n FROM Student WHERE n LIKE '刘%'; 能否执行? 答:不能(多数实现)。别名在 SELECT 阶段生成,而 WHERE 先于 SELECT 执行,应把条件写成 WHERE Sname LIKE '刘%'。
  3. ORDER BY 中使用别名是否可行? 答:可以。ORDER BY 在 SELECT 之后执行,能识别 SELECT 中定义的别名。

第 2 课 数据定义:模式、表与索引

对应教材 3.2。定义表就是定义"结构与约束"。

建表时要同时想清楚三件事:有哪些列、每列的数据类型、以及必须满足的完整性约束。 约束可以直接写在列后面(列级),也可以在所有列定义完之后单独写(表级)。 凡涉及多个列的约束(如多属性主码、外码)只能写成表级。

-- 模式(数据库下的命名空间)
CREATE SCHEMA "S-T" AUTHORIZATION WANG;
DROP SCHEMA "S-T" CASCADE;          -- CASCADE 连带删除 / RESTRICT 有依赖则拒绝

-- 基本表:列级约束
CREATE TABLE Student (
  Sno   CHAR(9)  PRIMARY KEY,                  -- 主码:非空且不重复
  Sname CHAR(20) UNIQUE,
  Ssex  CHAR(2)  CHECK (Ssex IN ('男','女')),
  Sage  SMALLINT NOT NULL,
  Sdept CHAR(20)
);

-- 基本表:表级约束(多属性主码 + 两个外码)
CREATE TABLE SC (
  Sno   CHAR(9),
  Cno   CHAR(4),
  Grade SMALLINT CHECK (Grade BETWEEN 0 AND 100),
  PRIMARY KEY (Sno, Cno),
  FOREIGN KEY (Sno) REFERENCES Student(Sno) ON DELETE CASCADE,
  FOREIGN KEY (Cno) REFERENCES Course(Cno)
);

ALTER TABLE Student ADD Sentrance DATE;        -- 增加列
ALTER TABLE Student ALTER COLUMN Sage INT;     -- 修改类型
DROP TABLE Student CASCADE;

-- 索引(系统自动使用,用户不能显式指定)
CREATE UNIQUE INDEX Stusno ON Student(Sno);
CREATE CLUSTERED INDEX Stusname ON Student(Sname);
DROP INDEX Stusno;
要点
  • 常用类型:CHAR(n) 定长、VARCHAR(n) 变长、INT / SMALLINT、DECIMAL(p,d)、DATE / TIME。
  • 外码的违约处理:NO ACTION(默认拒绝)、CASCADE(级联)、SET NULL(要求该列允许为空)。
  • 索引提高查询速度,但降低插入更新速度;一个表只能有一个聚簇索引。
课堂练习(3 题)
  1. 为什么 PRIMARY KEY (Sno, Cno) 不能写成列级约束? 答:它涉及两个列,列级约束只能约束所在列,必须写成表级约束。
  2. 删除 Student 中某学生时希望自动删除其选课记录,应如何定义外码? 答:在 SC 表的外码上加 ON DELETE CASCADE。
  3. DROP TABLE Student CASCADE; 与 RESTRICT 的后果有何不同? 答:CASCADE 连带删除依赖该表的视图与外码约束;RESTRICT 在存在依赖时拒绝执行。

第 3 课 单表查询

对应教材 3.3.1。选择列、过滤行、排序、统计分组。

单表查询是所有查询的基础。分成四步理解: ①投影(选哪些列,可含计算列与别名)→ ②选择(WHERE 保留哪些行)→ ③排序(ORDER BY)→ ④统计(聚集函数与 GROUP BY)。 空值用 IS NULL 判断,不能用等号。

-- ①投影与计算列
SELECT Sname, 2026 - Sage AS birth_year FROM Student;
SELECT DISTINCT Sdept FROM Student;               -- 去掉重复行

-- ②选择:条件表达式
SELECT Sname FROM Student WHERE Sdept = 'CS' AND Sage < 20;
SELECT Sname FROM Student WHERE Sage BETWEEN 20 AND 23;
SELECT Sname FROM Student WHERE Sdept IN ('CS','MA');
SELECT Sname FROM Student WHERE Sname LIKE '刘%';  -- % 任意长度,_ 单个字符
SELECT Sno  FROM SC WHERE Grade IS NULL;           -- 空值判断不能用 = NULL

-- ③排序
SELECT * FROM Student ORDER BY Sdept ASC, Sage DESC;

-- ④统计:聚集函数忽略 NULL
SELECT COUNT(*), COUNT(DISTINCT Sno), AVG(Grade), MAX(Grade) FROM SC;
SELECT Cno, COUNT(Sno) AS cnt, AVG(Grade) AS avg_g
FROM SC GROUP BY Cno HAVING AVG(Grade) >= 80;
要点
  • SELECT 中出现的所有非聚集列都必须出现在 GROUP BY 中。
  • COUNT(*) 统计行数(不忽略空行),COUNT(列) 跳过该列为空的行。
  • LIKE 的 % 与 _ 含义不同;查询含通配符的字面量需 ESCAPE 转义。
课堂练习(3 题)
  1. 查询名字为两个汉字且姓"欧阳"的学生。 答:SELECT Sname FROM Student WHERE Sname LIKE '欧阳__';(两个下划线各代表一个字符)。
  2. 统计各系人数,只显示人数大于 30 的系。 答:SELECT Sdept, COUNT(*) FROM Student GROUP BY Sdept HAVING COUNT(*) > 30;
  3. 为什么 WHERE Grade = NULL 查不到任何行? 答:与 NULL 的比较结果是 UNKNOWN,不是 TRUE,行不会被选中;必须写 Grade IS NULL。

第 4 课 连接查询

对应教材 3.3.2。把多张表按关联条件拼起来。

当所需信息分散在多个表中时,需要连接。内连接只保留能匹配上的行; 外连接保留一侧的全部行(未匹配的列填 NULL); 同一张表与自己连接叫自身连接,必须起别名才能区分两个实例。

-- 等值连接:两种写法等价
SELECT Student.Sno, Sname, Cno, Grade
FROM Student, SC WHERE Student.Sno = SC.Sno;

SELECT Student.Sno, Sname, Cname, Grade
FROM Student
JOIN SC     ON Student.Sno = SC.Sno
JOIN Course ON SC.Cno = Course.Cno;

-- 自身连接:查询每门课的间接先修课
SELECT FIRST.Cno, SECOND.Cpno
FROM Course FIRST, Course SECOND
WHERE FIRST.Cpno = SECOND.Cno;

-- 左外连接:保留未选课的学生
SELECT Student.Sno, Sname, Cno, Grade
FROM Student LEFT OUTER JOIN SC ON Student.Sno = SC.Sno;
要点
  • 漏写连接条件会得到笛卡尔积(行数 = 两表行数之积),是典型错误。
  • LEFT JOIN 保留左表全部行,RIGHT JOIN 保留右表,FULL OUTER JOIN 两侧都保留。
  • 多表连接时列名冲突必须加表名前缀限定。
课堂练习(3 题)
  1. 查询每个学生及其选课情况,包括一门课都没选的学生。 答:用左外连接 FROM Student LEFT OUTER JOIN SC ON Student.Sno = SC.Sno。
  2. 查询选修 2 号课程且成绩在 90 分以上的学生姓名。 答:SELECT Sname FROM Student JOIN SC ON Student.Sno = SC.Sno WHERE Cno = '2' AND Grade > 90;
  3. 为什么自身连接必须给表起别名? 答:同一张表出现两次,不取别名无法区分引用的是哪一个实例,列名也会产生二义性。

第 5 课 嵌套查询与集合查询

对应教材 3.3.3 与 3.3.4。用子查询表达"在某个集合里 / 对全部都成立"。

当一个查询的条件依赖另一个查询的结果时,就把后者写成子查询。 不相关子查询可以独立执行(先算内层,再算外层);相关子查询依赖外层当前行(如 EXISTS)。 表达"全部 / 所有"这类全称量词时,标准写法是双层 NOT EXISTS。

-- IN:先算内层集合,再判断外层是否属于它
SELECT Sname FROM Student
WHERE Sno IN (SELECT Sno FROM SC WHERE Cno = '2');

-- ANY / ALL:与某个值 / 与全部值比较
SELECT Sname, Sage FROM Student
WHERE Sage < ANY (SELECT Sage FROM Student WHERE Sdept = 'CS');   -- 比其中某个小
SELECT Sname FROM Student
WHERE Sage > ALL (SELECT Sage FROM Student WHERE Sdept = 'CS');   -- 比全部都大

-- EXISTS:相关子查询,只关心"有没有返回行"
SELECT Sname FROM Student
WHERE EXISTS (SELECT * FROM SC WHERE Sno = Student.Sno AND Cno = '1');

-- 全称量词:查询选修了全部课程的学生
SELECT Sname FROM Student
WHERE NOT EXISTS (
  SELECT * FROM Course
  WHERE NOT EXISTS (
    SELECT * FROM SC WHERE Sno = Student.Sno AND Cno = Course.Cno));
-- 语义:不存在这样一门课——该学生没有选它

-- 集合查询(列数相同、类型相容才能运算)
SELECT * FROM Student WHERE Sdept = 'CS'
UNION SELECT * FROM Student WHERE Sage <= 19;      -- 并,UNION ALL 保留重复
SELECT Sno FROM SC WHERE Cno = '1'
INTERSECT SELECT Sno FROM SC WHERE Cno = '2';      -- 交
SELECT Sno FROM Student EXCEPT SELECT Sno FROM SC; -- 差
要点
  • = ANY 等价于 IN;<> ALL 等价于 NOT IN。
  • EXISTS 子查询的 SELECT 列表习惯写 *,因为只看是否有行返回。
  • 双层 NOT EXISTS 是"全部"类题目的通用模板,务必背下来。
课堂练习(3 题)
  1. 查询没有选修 1 号课程的学生姓名。 答:SELECT Sname FROM Student WHERE NOT EXISTS (SELECT * FROM SC WHERE Sno = Student.Sno AND Cno = '1');
  2. 查询与"刘晨"在同一个系学习的学生。 答:SELECT Sname FROM Student WHERE Sdept IN (SELECT Sdept FROM Student WHERE Sname = '刘晨');
  3. UNION 与 UNION ALL 有何区别? 答:UNION 去掉重复行(需额外去重开销),UNION ALL 保留全部行(包括重复),效率更高。

第 6 课 数据更新与空值

对应教材 3.4 与 3.5。

插入、修改、删除都会触发完整性检查:违反主码、外码或 CHECK 时语句会被拒绝。 更新操作可以带子查询,用来描述"对满足某条件的一批行进行操作"。

-- 插入:单条与批量(子查询结果)
INSERT INTO Student (Sno, Sname, Ssex, Sdept, Sage) VALUES ('201215128','陈冬','男','IS',18);
INSERT INTO Deptage(Sdept, Avgage)
  SELECT Sdept, AVG(Sage) FROM Student GROUP BY Sdept;

-- 修改:全部、按条件、带子查询
UPDATE Student SET Sage = Sage + 1;
UPDATE Student SET Sage = 22 WHERE Sno = '201215121';
UPDATE SC SET Grade = 0
WHERE 'CS' = (SELECT Sdept FROM Student WHERE Student.Sno = SC.Sno);

-- 删除
DELETE FROM SC WHERE Sno = '201215128';
DELETE FROM SC
WHERE 'CS' = (SELECT Sdept FROM Student WHERE Student.Sno = SC.Sno);
DELETE FROM SC;                       -- 清空数据,保留表结构

-- 空值
SELECT Sno, Cno FROM SC WHERE Grade IS NULL;
SELECT Sno FROM SC WHERE Grade IS NOT NULL AND Grade < 60;
要点
  • 删除被参照表的行时若有外码引用会被拒绝,除非定义了级联删除——先删子表再删父表。
  • 空值参与算术运算结果为 NULL,参与比较得到 UNKNOWN(三值逻辑)。
课堂练习(2 题)
  1. 把计算机系全体学生的成绩置零,写出语句。 答:UPDATE SC SET Grade = 0 WHERE 'CS' = (SELECT Sdept FROM Student WHERE Student.Sno = SC.Sno);
  2. 删除"刘晨"同学时先做什么?为什么? 答:先删除 SC 中该同学的选课记录,再删 Student 中的记录;否则违反参照完整性(除非外码定义了 ON DELETE CASCADE)。

第 7 课 视图

对应教材 3.6。虚表:只存定义,不存数据。

视图是从一张或多张基本表导出的虚表,数据库中只保存它的定义。 查询视图时,DBMS 会做视图消解:把对视图的查询转换成对基本表的等价查询。 更新视图时,DBMS 需要把更新唯一地映射回基本表——因此由聚集、分组、DISTINCT 导出的视图通常不可更新。

CREATE VIEW CS_Student AS
  SELECT Sno, Sname, Sage FROM Student WHERE Sdept = 'CS'
  WITH CHECK OPTION;                  -- 通过视图更新时仍受视图条件约束

CREATE VIEW S_G(Sno, Gavg) AS         -- 分组视图:通常不可更新
  SELECT Sno, AVG(Grade) FROM SC GROUP BY Sno;

SELECT * FROM CS_Student WHERE Sage < 20;          -- 视图消解为对 Student 的查询
UPDATE CS_Student SET Sname = '刘辰' WHERE Sno = '201215121';
DROP VIEW CS_Student;
要点
  • 视图的作用:简化操作、多角度看待数据、提供逻辑独立性、安全保护、清晰表达查询。
  • 行列子集视图可更新;含聚集 / GROUP BY / DISTINCT 的视图不可更新。
  • WITH CHECK OPTION 保证通过视图插入或修改的行仍满足视图定义条件。
课堂练习(2 题)
  1. 为什么对 S_G(平均成绩视图)执行 UPDATE 会被拒绝? 答:该视图由 GROUP BY 与聚集函数导出,一行视图对应基本表中的多行,无法唯一映射回基本表。
  2. 为 CS_Student 加了 WITH CHECK OPTION 后,插入一个 Sdept 为 'MA' 的学生会怎样? 答:会被拒绝,因为插入结果不满足视图定义中的 Sdept = 'CS' 条件。

第 8 课 授权与事务

对应教材第 4 章(安全)与第 11 章(事务)。

授权解决"谁能做什么":自主存取控制用 GRANT 授予、REVOKE 收回,可按列授权,也可用角色批量管理。 事务解决"一组操作要么全做要么全不做":以 COMMIT 提交或 ROLLBACK 回滚结束,具备 ACID 特性。

-- 授权与收回
GRANT SELECT ON TABLE Student TO U1 WITH GRANT OPTION;   -- 允许 U1 再授权
GRANT UPDATE(Sage), SELECT ON TABLE Student TO U2, U3;   -- 列级权限
REVOKE UPDATE(Sage) ON TABLE Student FROM U2;

-- 角色:先授权给角色,再把角色授予用户
CREATE ROLE R1;
GRANT SELECT, UPDATE ON TABLE Student TO R1;
GRANT R1 TO 王平, 张明;

-- 命名约束,便于后续修改
CREATE TABLE Student (
  Sno NUMERIC(6) CONSTRAINT C1 CHECK (Sno BETWEEN 90000 AND 99999),
  CONSTRAINT StudentKey PRIMARY KEY (Sno)
);
ALTER TABLE Student DROP CONSTRAINT C1;

-- 事务
BEGIN TRANSACTION;
  UPDATE Account SET balance = balance - 100 WHERE id = 'A';
  UPDATE Account SET balance = balance + 100 WHERE id = 'B';
COMMIT;                 -- 任一步出错则执行 ROLLBACK;
要点
  • WITH GRANT OPTION 表示被授权者可继续转授;收回时需写 CASCADE 才会级联收回。
  • ACID:原子性、一致性、隔离性、持久性;事务是恢复与并发控制的基本单位。
课堂练习(2 题)
  1. U1 把权限转授给了 U2,回收 U1 的权限后 U2 是否仍有权限? 答:若 REVOKE 未写 CASCADE,U2 仍保留权限;写 CASCADE 才会一并收回。
  2. 转账过程中第二步更新失败,应如何处理? 答:执行 ROLLBACK 回滚事务,使数据库回到 BEGIN 之前的状态,保证原子性。

阶段自测

五道综合题,覆盖前八课的核心写法。先自己写,再展开对照。

查看题目与参考答案(点击折叠)
  1. 查询选修 2 号课程且成绩不低于 90 分的学生姓名与成绩。 答:SELECT Sname, Grade FROM Student JOIN SC ON Student.Sno = SC.Sno WHERE Cno = '2' AND Grade >= 90;
  2. 统计每门课程的选课人数与平均分,只显示平均分不低于 80 的课程。 答:SELECT Cno, COUNT(Sno), AVG(Grade) FROM SC GROUP BY Cno HAVING AVG(Grade) >= 80;
  3. 查询一门课都没有选修的学生姓名。 答:SELECT Sname FROM Student WHERE NOT EXISTS (SELECT * FROM SC WHERE Sno = Student.Sno);
  4. 把计算机系(CS)全体学生的年龄加 1。 答:UPDATE Student SET Sage = Sage + 1 WHERE Sdept = 'CS';
  5. 建立"计算机系学生"视图,并说明它是否可更新。 答:CREATE VIEW CS_Student AS SELECT Sno, Sname, Sage FROM Student WHERE Sdept = 'CS'; 它属于行列子集视图,可以更新;若定义中含聚集函数或 GROUP BY 则不可更新。