最近在学数据库系统,把课上反复出现的 SQL 写法、设计题思路和范式分析串成一篇笔记,方便自己以后回看,也分享给同样在学这门课的朋友。下文以 MySQL 8.0 为主要参考,字段名若含 #、中文或关键字,建议用反引号包裹。
一、SQL 在干什么:建表、改表与增删改
1. 建表时常见约束
教学库里典型的三张表是 学生 S、课程 C、选课 SC。建表时我一般会同时想清楚主键、外键和检查约束:
CREATE TABLE S ( `S#` VARCHAR(10) PRIMARY KEY, SNAME VARCHAR(50) NOT NULL, AGE INT CHECK (AGE BETWEEN 0 AND 120), SEX CHAR(1) CHECK (SEX IN ('M','F','男','女')));
CREATE TABLE SC ( `S#` VARCHAR(10), `C#` VARCHAR(10), GRADE DECIMAL(5,2), PRIMARY KEY (`S#`, `C#`), FOREIGN KEY (`S#`) REFERENCES S(`S#`), FOREIGN KEY (`C#`) REFERENCES C(`C#`), CHECK (GRADE IS NULL OR GRADE BETWEEN 0 AND 100));ALTER TABLE 用来加列、改类型、删列、加重命名;MySQL 8+ 可用 RENAME COLUMN。插入、更新、删除就是熟悉的 INSERT / UPDATE / DELETE。
2. 单表查询的几个固定套路
- 条件过滤:
WHERE - 分组统计:
GROUP BY+HAVING(过滤的是分组后的结果,不是行) - 排序分页:
ORDER BY,LIMIT n OFFSET m
例如「统计每门课选修人数,只显示超过 15 人的课,按人数降序、课号升序」:
SELECT `C#`, COUNT(*) AS 人数FROM SCGROUP BY `C#`HAVING COUNT(*) > 15ORDER BY 人数 DESC, `C#` ASC;3. 多表查询:连接与子查询
显式 JOIN 可读性通常更好:
SELECT s.SNAME, sc.GRADEFROM S sJOIN SC sc ON s.`S#` = sc.`S#`WHERE sc.`C#` = 'C1'ORDER BY sc.GRADE DESC;「某同学不学的课程」这类题,我更喜欢 NOT EXISTS,语义接近「不存在选课记录」:
SELECT c.`C#`FROM C cWHERE NOT EXISTS ( SELECT 1 FROM S s JOIN SC sc ON s.`S#` = sc.`S#` WHERE s.SNAME = 'WANG' AND sc.`C#` = c.`C#`);NOT IN 也能写,但要注意子查询里出现 NULL 时结果可能不符合直觉;考试和工程里 NOT EXISTS 更稳。
聚合在多表场景很常见,例如「李文老师所授每门课的平均分与最高分」:
SELECT c.`C#`, c.CNAME, AVG(sc.GRADE) AS 平均成绩, MAX(sc.GRADE) AS 最高成绩FROM C cJOIN SC sc ON c.`C#` = sc.`C#`WHERE c.TEACHER = '李文'GROUP BY c.`C#`, c.CNAME;二、关系代数与 SQL 的对应(心里有个映射)
| 关系代数 | SQL 里大致对应 |
|---|---|
| 选择 σ | WHERE |
| 投影 π | SELECT 列 |
| 连接 ⋈ | JOIN |
| 并 ∪ | UNION |
| 差 − | NOT EXISTS / 外连接过滤 |
比如「WANG 不学的课号」用代数可写成:。写 SQL 时不一定要先写代数,但差集类问题先想「全集减去已选集合」会少绕弯。
三、数据库设计:从 E-R 到关系模式
设计题的核心步骤我习惯固定成三步:
- 找实体:各自有哪些属性,谁是主标识(主键候选)。
- 找联系:1<1>1>、1
、M ,联系有没有自己的属性(如借阅日期、成绩)。 - 转关系模式:实体各成一张表;1
把「一」方主键放进「多」方作外键;M 单独建联系表,主键常是两端主键的组合。
图书借阅例子(简化):
- 出版社 —(1
)— 书籍:出版社名可放进书籍表作外键。 - 借书人 —(M
)— 书籍:拆成 借阅(书号,借书证号,借书日期,还书日期),两端都是外键。
运动会里「一个裁判组只负责一个比赛项目」往往是 1<1>1>:可以把裁判组编号放进比赛项目表,并加 UNIQUE 保证一个裁判组只对应一个项目。
商品进销存里供应商—商品、商店—客户—商品可能出现 三元联系,最终也会落成带多个外键的联系表,并想清楚候选码(例如是否要把「销售日期」放进主键,避免同一天多笔交易冲突)。
四、范式:为什么要拆表
一张「大而全」的表容易出现四类问题:
- 冗余:同一学生姓名、系别因多门课重复多行。
- 插入异常:还没选课的学生,若主键是(学号+课程号),连基本信息都难插。
- 删除异常:删掉某人选的最后一门课,可能连人带课信息一起丢。
- 更新异常:改名、改系名要改很多行,容易不一致。
判断范式时我会先写 函数依赖 FD 和 候选码:
- 选课大表 R(学号,姓名,年龄,系别,课程号,课程名,成绩)
- 学号 → 姓名,年龄,系别
- 课程号 → 课程名
- (学号,课程号)→ 成绩
- 候选码:(学号,课程号)
- 存在部分依赖(学号、课程号各自决定非主属性)→ 至少要做 3 张表:学生、课程、选课。
学生表带系名(学号→系号,系号→系名)会出现传递依赖,属于 2NF 但不到 3NF,应拆出 系(系号,系名) 和 学生(学号,姓名,年龄,性别,系号)。
库存(仓库号,地点,设备号,设备名,库存数量) 典型拆法:
- 仓库(仓库号,地点)
- 设备(设备号,设备名)
- 库存(仓库号,设备号,库存数量)
五、视图、存储过程与触发器(会用即可)
视图适合统计展示,例如按读者统计借阅册数:
CREATE OR REPLACE VIEW v_bnum (`读者号`, `借阅数量`) ASSELECT r.`读者号`, COUNT(b.`图书号`)FROM `读者` rLEFT JOIN `借阅` b ON r.`读者号` = b.`读者号`GROUP BY r.`读者号`;触发器常用来维护一致性:学号在 S 表变更时同步 SC;删学生时级联删成绩;订单插入后扣减商品库存。写之前要弄清 BEFORE / AFTER 以及 NEW / OLD 的含义。
存储过程 / 函数适合封装「按订单号算总金额」「按学号返回性别」这类重复逻辑;注意 DELIMITER 和 IN / OUT 参数。
六、工程里还会碰到什么
课上综合题常把几块拼在一起,我自己记成四句话:
- 性能:给
WHERE、JOIN、ORDER BY、GROUP BY高频列建合适索引;用EXPLAIN看计划;少SELECT *。 - 安全:最小权限、
GRANT/REVOKE、角色;敏感字段加密;视图隐藏列;审计日志。 - 并发:事务 ACID;隔离级别与脏读、不可重复读、幻读;选课、库存用事务 + 行锁 / 条件更新防超卖。
- 备份恢复:全量 + 增量/binlog;主从提高读能力;定期做恢复演练。
选课高峰、二手平台大促这类场景,本质都是:索引扛查询、事务扛一致性、备份扛故障。
写在最后
对我来说,数据库这门课适合按 SQL 熟练 → 多表与聚合 → E-R 设计 → 范式分解 → 视图/过程/触发器 → 性能与安全 这条线推进。教学库 S/C/SC、图书借阅、供应商零件 SP、集团人事 EMP/WORKS/COMP 这几类模型反复出现,把「不学的课」「没供应的零件」「没任职的职工」写对,大部分查询题就有套路了。
如果你也在学数据库,欢迎一起交流;文中有错漏也请在评论区指正。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时





