数据库系统概论基础笔记:关系模型、SQL、范式与事务并发
数据库系统概论这门课我前后啃了两遍才真正摸到门道。第一遍跟着教材从数据模型背到范式考试能过但一上手写表结构还是发虚写出来的 SQL 能跑可一问为什么这么建就卡壳第二遍我干脆把整门课按概念—模型—语言—设计—控制重排了一遍做成了这份基础篇笔记才算是把这些散落的知识点串成了一条线。这份笔记想干的事不复杂把教材里那些看着抽象的名词翻译成建表、写查询、做设计时真正用得上的判断依据顺带把当年踩过的坑标出来。它适合三类人——正在学这门课、需要一份能配合教材复习的提纲的学生已经工作但表结构设计总感觉别扭、想补一补底层逻辑的开发者以及准备面试、需要把范式、事务、索引这些高频考点讲清楚的人。基础篇的范围也就到这数据模型、关系模型、关系代数与 SQL、规范化、事务与并发控制不涉及分布式、查询优化器内部实现这些深水区。1. 这份笔记为什么要这样整理1.1 从背概念到建心智模型的转变教材的目录顺序是按学科逻辑排的先讲数据模型的发展史再讲关系模型然后关系代数、SQL、规范化、事务。这个顺序对写书的人很顺对第一次学的人其实不太友好因为你不知道每个概念后面会被谁用到。我第一次学的时候看到外模式/模式映像完全没感觉只觉得是个要背的名词后来真正做项目需要给不同角色开不同权限的视图、又不想让上层应用跟着物理表结构改动而重写时才回头理解这个机制存在的意义。所以这份笔记的重排原则是每个概念必须先回答它解决什么问题。三级模式回答的是程序和数据怎么解耦关系代数回答的是查询为什么可以被优化范式回答的是冗余为什么会产生更新异常事务回答的是并发时数据为什么不能乱。这四句话记住了后面所有细节都是往这个骨架上挂东西。具体做法是每学完一章我不抄定义只写一句这章在防止什么样的错误发生然后补一个自己脑子里能跑起来的例子。比如学完函数依赖我写的不是X→Y而是学号一确定姓名就确定所以姓名不该另建一张表存。这个转变最直接的收益是做题速度。考卷上那种判断该关系属于第几范式的题如果只背定义得一点点对着条文核如果脑子里有部分依赖传递依赖的具象例子扫一眼候选键和非主属性就能判。工作以后更明显看别人给的表结构能立刻感觉到哪里冗余了、哪里会埋更新异常。我的建议是笔记里每个知识点下面强制留一行反例。定义写一遍就够了反例才是你真正理解的地方。1.2 笔记的骨架三条主线一条辅线整份基础篇笔记我拆成了三条主线和一条辅线。主线一是抽象线数据模型、三级模式、ER 模型、关系模型讲的是现实世界怎么变成一张表主线二是语言线关系代数、SQL讲的是想取的数据怎么表达出来主线三是设计线函数依赖、范式、分解讲的是表怎么切才不出问题。辅线是控制线事务、并发、封锁讲的是多个人同时动数据怎么不乱。控制线之所以叫辅线是因为基础篇里它只讲原理和经典结论不展开具体数据库的实现差异。这三条线之间是有先后依赖的不理解关系模型里的键就没法判断函数依赖不判断函数依赖就没法判范式不理解范式看到 SQL 里的 JOIN 就只会照抄不知道为什么要拆表。当年我就是顺序乱了先学了 SQL 语法回头补关系代数时特别痛苦因为脑子里已经先入为主地把 SQL 当成唯一的表达方式再看 σ、π 这种符号完全建立不起对应关系。所以笔记的阅读顺序建议是先花半天把第 2 章的三级模式和 ER 转换吃透这是全局地图然后第 3 章的关系模型与键必须吃透这是后面所有判定的基础第 4 章关系代数和 SQL 对照着看效果最好第 5 章范式放在 SQL 之后因为你已经见过拆表长什么样了。第 6 章事务可以独立看不依赖前面太多。1.3 工具和材料的准备别在这上面浪费时间基础篇阶段其实不需要折腾太多工具。我的配置是一份教材加一份课堂讲义当主线一个能本地跑起来的关系型数据库用来验证 SQL再加一个纯文本的笔记文件。数据库这块选一个安装简单、语法贴近标准 SQL 的就行主流开源关系库的差异在基础篇阶段完全不影响学习。真正要注意的是别一上来就装重型环境我见过有同学为了学这门课去配集群、配容器编排三天过去了第一条 CREATE TABLE 还没跑通学习热情先消耗完了。笔记工具上我踩过一个坑早期用带双链的笔记软件每个概念都要建一个节点结果关系图画得很漂亮但从来没回头复习过。后来改成单文件、按章节分块、每块下面固定三行——是什么、为什么、反例效率反而高。因为这门课的复习本质上是回忆 自测不是检索链接跳来跳去反而打断思路。还有一点SQL 一定要亲手敲不能只看。看例题的时候觉得这不就是 JOIN 一下自己写的时候才会发现 GROUP BY 和 HAVING 的位置、NULL 的比较、多表连接的歧义这些细节全会出错。我在笔记里专门留了一页手写错误记录把每次写错的 SQL 原样贴进去旁边写正确版本和错因这一页是整份笔记里复习频率最高的。2. 三级模式与数据模型先把全局地图画出来2.1 数据模型的三层抽象到底在抽象什么数据模型这个词很容易被当成一个笼统的术语混过去其实它有三个明确的层次而且这三层是逐个抽象、逐层服务不同角色的。概念模型面向用户和设计者用的是人话典型代表就是 ER 模型画出来的是实体、属性、联系业务方看得懂逻辑模型面向数据库设计者和应用开发者典型代表是关系模型落成的是有哪些表、每张表有哪些列、列之间怎么关联物理模型面向具体的存储实现管的是数据文件怎么组织、索引怎么建、页怎么划分。这三层的价值在于变更成本递减。业务规则变了先改概念模型再往下传导如果直接改物理层上层全得跟着动。我当年做一个选课相关的小系统一开始图省事把课程容量直接写死在应用代码的判断里后来要加候补逻辑才发现这个概念在模型层根本没体现只能到处打补丁。如果当初在概念模型阶段就把选课记录和课程容量的关系画清楚后面改动会集中得多。这里有个常见误解要澄清ER 模型不是画着玩的图它是要被转换成关系模型的中间产物所以画的时候就得考虑转换的可行性。比如三元联系、自反联系、弱实体画的时候很自然转换的时候就要多想一步。我建议画 ER 图时手边就放一张转换规则速查边画边想它最后会变成几张表。2.2 三级模式两级映像程序和数据解耦的那点事三级模式是外模式、模式、内模式。用生活化的话说内模式是仓库里货架怎么摆模式是整个仓库有哪些货、怎么编号外模式是不同岗位的人看到的那个取货清单——财务只需要看金额和日期库管只需要看货位和数量各自看到的都是同一批数据的不同切面。两级映像就是这两层之间的翻译层。外模式/模式映像保证的是逻辑独立性模式变了比如给某张表加了一列只要外模式不变基于外模式写的应用就不用改。模式/内模式映像保证的是物理独立性内模式变了比如换存储结构、加索引、调整文件组织只要模式不变上层完全无感。这两个独立性不是背书用的它们在实操里有非常具体的对应物。逻辑独立性对应的是视图给不同角色建不同的视图物理表怎么改视图对外接口保持稳定。物理独立性对应的是索引和存储参数调整给常用查询加个索引SQL 语句一个字不用改执行效率就变了。理解到这一层你就明白为什么教材要花一整节讲视图——它不只是简化查询更是逻辑独立性的落地手段。注意视图带来的独立性是有代价的。多层嵌套视图会让执行计划变复杂更新型视图还受一堆限制。基础篇阶段记住视图是逻辑独立性的实现手段这层就够了。2.3 ER 模型转关系模型四条规则和三个坑转换规则本身不复杂教材上四条实体转为关系实体的属性成为关系的属性实体的码成为关系的码一对一联系可以合并到任一端一对多联系把一端的码放到多端作为外码多对多联系单独建一个关系两端码的组合作为码。规则好背坑在细节。第一个坑是多对多联系的联系属性。比如选课这个联系如果它带一个成绩属性这个属性既不属于学生也不属于课程必须放到新建的选课关系里。我见过有人把成绩塞进学生表结果一个学生只能存一个成绩直接错。第二个坑是一对一联系的取舍。合并到哪一端不是随意的要看业务上哪一端更必须存在、哪一端查询更频繁还要考虑 NULL 的比例。第三个坑是弱实体它没有自己的码必须依赖强实体的码转换时要把强实体的码一起带过来组成复合码。举个具体例子。学生学号姓名专业和课程课程号课程名学分选课联系带成绩且一个学生可以选多门课一门课可以被多个学生选。转换结果三张表学生表以学号为主码课程表以课程号为主码选课表以学号课程号为复合主码成绩作为普通属性。如果还存在教师—课程的一对多一门课只由一个教师主讲那就是把教师工号放到课程表里当外码而不是反过来。这个转换过程建议在纸上画一遍再动手建表。我自己的习惯是画完 ER 图后在旁边直接写出目标表结构和每张表的码写不出来说明联系的类型没判断清楚回去重画。这比建完表再改要省事得多。3. 关系模型与键后面所有判定的地基3.1 关系、元组、属性、域术语先对齐关系模型里有一套自己的术语关系就是表元组就是行属性就是列域就是某一列取值的集合。听起来是废话但考试和面试都爱在这上面绕。比如关系的度指的是属性的个数关系的基数指的是元组的个数这两个词经常被混着用分量指的是元组在某个属性上的取值关系模式是对关系的描述写成 R(U, D, DOM, F)而关系是某一时刻的具体数据是会变的。还有一个必须提前说清楚的点关系模型要求属性不可再分这是第一范式的要求也是整个模型的前提。现实里经常遇到地址这种字段如果业务上需要按省市区查询那就必须拆成三个属性不能塞成一个字符串然后在应用层切。这不是数据库死板而是因为一旦允许嵌套关系代数的运算就没法定义了。NULL 的处理也是术语层面的坑。NULL 不是 0也不是空字符串它表示未知或不适用。任何和 NULL 的比较运算结果都是未知不是真也不是假所以WHERE 成绩 NULL永远查不出东西。这个点后面在 SQL 那章还会展开。3.2 候选键、主键、外键判定方法比定义重要超码是能唯一标识一个元组的属性集候选键是最小的超码也就是去掉任何一个属性都不能唯一标识主键是从候选键里选出来的那一个通常选最简单、最稳定的外键是引用另一张表主键的属性组。判定的实操方法是拿到一张表的函数依赖集先求每个属性的闭包闭包包含全部属性的那些属性集就是超码再从超码里挑最小的。举个经典例子R(学号, 姓名, 系名, 系主任)函数依赖是 学号→姓名学号→系名系名→系主任。学号的闭包是{学号,姓名,系名,系主任}包含了全部属性所以学号是候选键系名的闭包是{系名,系主任}不包含全部所以系名不是。候选键只有一个主键就选学号。外键的判定要结合业务语义不能只看字面。比如选课表里的课程号它引用课程表的主键那它就是外键但如果某张表里有个课程号字段只是记录一个文本编号、课程表根本不存在那它逻辑上就不是外键约束。这个区别在实操里很重要——外键约束是数据库层面的强制检查不是命名约定。主键的选择也有讲究。业务上学号看起来很稳定但真遇到学号规则调整改起来就是全表波及用自增代理键做主键、把学号加唯一约束是工程上更常见的做法。基础篇阶段教材一般直接用自然键这是为了教学简洁实际项目里两种都有理解各自的取舍就行。3.3 三类完整性约束数据库帮你挡掉什么实体完整性要求主码非空且唯一这是数据库层面强制执行的参照完整性要求外码的取值要么为 NULL要么必须存在于被参照关系的主码中也是强制执行的用户定义完整性是业务规则比如成绩必须在 0 到 100 之间靠 CHECK 约束或应用层实现。这三类完整性是数据库的守门员但守门员不是万能的。参照完整性只在声明了外键约束的情况下生效很多实际系统出于性能或分库分表的考虑故意不建外键靠应用层保证这时候数据库就挡不住了。用户定义完整性更依赖应用层因为业务规则会变而改约束往往需要锁表。这里有个实操细节值得记删除被参照表中的一行时参照完整性会阻止操作默认 RESTRICT除非声明了级联删除CASCADE或置空SET NULL。这两种策略的语义完全不同——级联删除意味着主记录没了从记录也没意义置空意味着从记录还在只是暂时失去归属。选错了会出现数据莫名消失或者一堆孤儿记录。我在笔记里把这个点单独列了一行设计外键时先想清楚删除语义再选策略。4. 关系代数与 SQL一套逻辑的两种写法4.1 五个基本运算别只记符号关系代数的基本运算是选择 σ、投影 π、并 ∪、差 −、笛卡尔积 ×。后面所有的交、连接、除都能由这五个推出来。教材强调基本两个字是有理由的它说明关系代数是封闭且完备的运算的结果仍然是关系可以继续参与运算这就为查询优化提供了理论基础——既然任何查询都能表达成运算树的组合那就可以对这棵树做等价变换找出代价更低的执行方式。这是我当年最大的一个认知升级点。之前我以为 SQL 就是背语法关系代数就是考试符号两者没关系。后来才明白优化器做的事本质就是对关系代数表达式做重写选择下推、投影下推、连接顺序调整都是在运算树上动刀。理解了这一点看执行计划时就不慌了。举几个必须能一眼转换的例子。选择 σ 对应 WHERE投影 π 对应 SELECT 列表但要注意投影会去重SQL 的 SELECT 默认不去重去重要写 DISTINCT笛卡尔积 × 对应不带连接条件的多表查询结果行数是乘积通常不是想要的自然连接对应 JOIN ... USING 或同名列等值连接。4.2 把关系代数翻译成 SQL翻译的关键是运算顺序。关系代数里通常先做选择、再做连接、最后投影SQL 的书写顺序却是 SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY逻辑执行顺序又不一样。笔记里我画了一张对照表复习时扫一眼就能对上。关系代数运算符号SQL 对应关键注意点选择σWHERE条件里不要写聚合函数投影πSELECT 列 DISTINCT默认不去重并∪UNION自动去重UNION ALL 不去重差−EXCEPT 或 NOT EXISTS注意 NULL 的影响笛卡尔积×CROSS JOIN 或省略连接条件行数是乘积慎用自然连接⋈JOIN ... USING / NATURAL JOIN同名列会合并容易误伤分组聚合γGROUP BY 聚合函数非聚合列必须在 GROUP BY 里一个具体的例子查询选修了课程号为 C1 的学生的姓名。关系代数写法是 π姓名(σ课程号C1(学生 ⋈ 选课))。SQL 写法是SELECT DISTINCT 姓名 FROM 学生 JOIN 选课 ON 学生.学号 选课.学号 WHERE 课程号 C1;。注意这里加了 DISTINCT因为一个学生可能有多条选课记录不去重会出现重复姓名。这个细节不亲手写一遍很容易漏。再举一个差运算的例子查询没有选修任何课程的学生。关系代数写法是 π学号(学生) − π学号(选课)。SQL 写法有两种NOT EXISTS或NOT IN。这两种在 NULL 存在时行为不同NOT IN遇到 NULL 会返回空结果集这是经典坑后面问题排查那节还会提。4.3 连接运算的坑自然连接和等值连接别混自然连接会自动找同名列做等值匹配并合并列等值连接需要显式写条件且保留两边的列。听起来差别不大实际用起来差别很大。比如学生表和选课表都有学号自然连接没问题但如果选课表里还有一个课程号而课程表里也有课程号三表自然连接时可能产生意料之外的匹配。更隐蔽的是如果两张表恰好有一个业务上无关但同名的列比如都叫备注自然连接会拿它当连接条件结果直接错。我的建议很直接生产 SQL 里尽量用显式 JOIN ON不用 NATURAL JOIN。显式写条件虽然啰嗦但可读性和可控性都强得多出了问题也容易定位。外连接是另一个重点。内连接只保留匹配成功的行左外连接保留左表全部行、右表不匹配的补 NULL。什么时候用外连接典型场景是统计每个学生的选课数包括没选课的学生。这时候必须用左外连接从学生表出发否则没选课的学生直接消失了。这个场景在实际报表里极其常见我见过不少人第一版写错发现数字对不上才回头改。5. 函数依赖与范式设计阶段最容易翻车的地方5.1 函数依赖的判定先从语义出发再用公理推函数依赖 X→Y 的意思是X 的取值唯一确定 Y 的取值。判定它只能靠业务语义不能靠数据抽样。这里有个特别容易犯的错误看到一张表的现有数据里系名和系主任是一一对应的就断言 系名→系主任。数据只能证伪不能证实样本里没出现反例不代表不存在。正确的做法是问业务方一个系是不是只有一个主任答案是肯定的才能写这条依赖。判定完之后要用 Armstrong 公理做推导求出属性闭包进而求候选键。自反律、增广律、传递律这三条要熟因为求闭包就是反复用传递律。举个手算过程R(A,B,C,D)F{A→B, B→C, A→D}。求 A 的闭包初始{A}由 A→B 得{A,B}由 B→C 得{A,B,C}由 A→D 得{A,B,C,D}闭包包含全部属性所以 A 是候选键。这个手算过程我建议练到不假思索因为判范式、判无损分解全靠它。还有两类特殊依赖要认出来部分函数依赖是指 X→Y 但存在 X 的真子集也能确定 Y这是第二范式要消除的传递函数依赖是指 X→Y、Y→Z 且 Y 不包含 X、Z 不属于 Y这是第三范式要消除的。把这两个定义和具体例子绑在一起记比背文字定义牢靠得多。5.2 从 1NF 到 BCNF每一级到底在治什么病套用前面的思路每一级范式都是在治一种具体的病。1NF 治的是属性可再分病象是一个字段里塞了多个值查询只能靠字符串匹配2NF 治的是非主属性对码的部分依赖病象是复合主键的表里某些列只依赖主键的一部分导致同一信息重复存储3NF 治的是非主属性对码的传递依赖病象是A 决定 B、B 决定 CC 跟着 B 重复BCNF 治的是主属性对码的部分和传递依赖是 3NF 的加强版要求每个决定因素都包含码。用那个经典例子走一遍R(学号, 姓名, 系名, 系主任)函数依赖 学号→姓名学号→系名系名→系主任候选键是学号。这里 学号→系名、系名→系主任系主任传递依赖于学号所以不满足 3NF。问题在哪同一个系有多个学生时系主任的名字会重复存多遍换系主任时要更新多行漏一行就数据不一致一个系暂时没有学生时系主任的信息无处可存。这三种现象分别叫冗余、更新异常、插入异常是范式理论真正的动机。分解方案是拆成 R1(学号, 姓名, 系名) 和 R2(系名, 系主任)。拆完之后两张表都满足 3NF 甚至 BCNF异常消失。5.3 无损连接和保持依赖分解不是随便拆拆表有两个硬指标。无损连接是指拆开再自然连接回去结果和原来一模一样不能多行也不能少行保持依赖是指原来的函数依赖在拆分后仍然能被各张表的依赖蕴涵否则约束检查就得跨表做代价高还容易漏。无损连接的判定有个好用的定理把 R 分解成 R1 和 R2如果 R1∩R2 → R1−R2 或者 R1∩R2 → R2−R1 成立分解就是无损的。以上面的例子验证R1∩R2 {系名}R1−R2 {学号, 姓名}R2−R1 {系主任}。系名→系主任成立所以无损。这个定理在考试里几乎是必考一定要会手算。保持依赖的判定要稍微麻烦一点需要把每个函数依赖投影到各张表上看能不能推出原依赖集。这里有个反直觉的结论存在满足 3NF 且既无损又保持依赖的分解但 BCNF 在保持依赖上不一定能做到。所以工程上经常停在 3NF不是不懂 BCNF而是权衡后的选择。实操心得范式是设计参考不是教条。实际项目里为了查询性能适度的反范式比如在订单表里冗余存一份用户名是常见做法前提是你清楚冗余的代价——更新时必须同步否则就等着数据对不上。6. 事务与并发控制多人同时操作时靠什么兜底6.1 ACID 的落地含义别停在缩写上原子性说的是事务里的操作要么全做要么全不做靠的是日志和回滚一致性说的是事务执行前后数据库都处于合法状态这个更多是靠约束和业务逻辑保证数据库只能兜一部分隔离性说的是并发事务之间互不干扰靠的是锁或快照持久性说的是提交之后就算断电也不丢靠的是日志先写盘。这四个里隔离性是唯一一个可以调档的也是最容易出问题的。原子性和持久性是底线没得商量一致性是目标隔离性是可配置的配置得低能换性能但会引入并发异常。教材讲隔离级别本质就是在讲你愿意用哪种异常换多少并发度。我在笔记里给每个特性配了一个反例原子性的反例是转账只扣了款没入账一致性的反例是外键指向了不存在的记录隔离性的反例是两个人同时抢最后一件库存都读到了还剩 1持久性的反例是提交成功但重启后数据没了。反例记住了概念就不会混。6.2 三种并发异常和四个隔离级别脏读是读到了别人还没提交的数据对方一回滚你读到的就是幻觉不可重复读是同一个事务里两次读同一行结果不一样因为中间被别人改了幻读是同一个事务里两次按同样条件查第二次多出了新行因为别人插入了。四个隔离级别从低到高是读未提交啥都不防、读已提交防脏读、可重复读防脏读和不可重复读、串行化全防。这里有个必须记住的细节标准的可重复读理论上防不住幻读但主流数据库的实现里可重复读往往通过快照机制连幻读也一并防住了具体行为要看文档。基础篇考试按教材的标准结论答工程上按实际数据库的行为来这两者不要混。隔离级别脏读不可重复读幻读大致代价读未提交可能可能可能最低读已提交不会可能可能较低可重复读不会不会标准定义下可能中等串行化不会不会不会最高隔离级别的选择没有标准答案看业务。对账、扣库存这种绝对不能错的往上调日志、统计这种能容忍一点偏差的往下调换吞吐。我自己的经验是默认级别通常够用真正出问题的地方往往不是隔离级别选错而是事务范围划得太大——一个事务里又调远程接口又做多表更新锁持有时间长并发一上来就互相等。6.3 封锁协议两段锁和三级协议怎么记封锁的基本单位是数据项共享锁 S 允许别人读不允许写排他锁 X 不允许别人读也不允许写。一级封锁协议是修改前必须加 X 锁、直到事务结束才释放能防丢失修改二级在一级基础上要求读之前加 S 锁、读完就放能防脏读三级要求读之前加 S 锁、直到事务结束才放能防不可重复读。两段锁协议说的是事务分两个阶段扩展阶段只能加锁不能解锁收缩阶段只能解锁不能加锁。遵守两段锁能保证并发调度的可串行化但不能避免死锁。这个不能避免死锁是高频考点也是高频误解点——很多人以为两段锁能解决一切其实它只保证正确性不保证活性。死锁的预防有两大类思路一是预防比如一次性申请所有锁、或者给数据项排序按序申请二是检测用等待图找环发现环就回滚代价小的事务。基础篇阶段掌握这两个思路的名字和原理就行不用深挖具体算法。7. 动手实操把笔记变成能跑的东西7.1 建表与约束落地光看理论容易飘我把笔记里的例子全部落成了可执行的 SQL。以选课场景为例标准 SQL 写法大致是这样CREATE TABLE student ( sno CHAR(10) PRIMARY KEY, sname VARCHAR(30) NOT NULL, major VARCHAR(40), CONSTRAINT uk_sname UNIQUE (sname) ); CREATE TABLE course ( cno CHAR(6) PRIMARY KEY, cname VARCHAR(40) NOT NULL, credit DECIMAL(3,1) CHECK (credit 0 AND credit 10) ); CREATE TABLE enrollment ( sno CHAR(10) NOT NULL, cno CHAR(6) NOT NULL, grade DECIMAL(5,2) CHECK (grade BETWEEN 0 AND 100), PRIMARY KEY (sno, cno), FOREIGN KEY (sno) REFERENCES student(sno), FOREIGN KEY (cno) REFERENCES course(cno) );这段建表语句里有几个值得说的点。主键用 CHAR 还是自增整数取决于业务学号这种定长编码用 CHAR 合理NOT NULL要显式写因为很多数据库的列默认允许 NULL而 NULL 会让唯一约束和比较运算的行为变得微妙CHECK 约束能挡掉明显非法的数据但不要指望它做复杂业务校验。最容易被忽略的是外键的删除策略默认是 RESTRICT也就是有选课记录的学生删不掉这个行为恰恰是参照完整性的体现不是 bug。建完表之后我会做一件事故意插入一条违反约束的数据看报错信息长什么样。这样以后在真实环境里看到类似报错能立刻定位是哪条约束。这个习惯帮我省过不少时间。7.2 索引和查询计划先看再动手索引这块基础篇讲得不深但有个动作必须做看一次执行计划。在查询前加上查看执行计划的命令能直观看到数据库是走全表扫描还是走索引。-- 观察查询是否使用了索引 EXPLAIN SELECT sname FROM student WHERE sno 2023000001;如果主键查询显示走了索引说明主键索引在起作用如果把 WHERE 条件换成一个没建索引的列通常就会看到全表扫描。这个对比做过一次你就永远记得索引是为查询条件服务的不是越多越好。索引的取舍逻辑也要理清索引加速读、拖慢写因为每次插入更新都要维护索引结构区分度低的列建索引收益很小比如性别只有两个值扫一半的表跟不建索引差别不大复合索引有最左前缀的原则把选择性高的列放前面通常更划算。这些结论在基础篇阶段知道为什么就够了具体的索引调优是后面的事。7.3 一个完整的规范化改造案例我把第 5 章的例子完整走了一遍从问题数据到分解再到验证。原始表是这样的学号姓名系名系主任课程号成绩001甲计算机系某导师AC188001甲计算机系某导师AC292002乙计算机系某导师AC175003丙数学系某导师BC381这张表的问题一眼能看出来系主任的名字跟着每一行重复甲选了两门课就存了两遍计算机系/某导师A如果某导师换人得更新所有相关行如果要记录一个还没有学生的系这张表根本存不进去因为主码里有学号。分解方案是拆成三张学生系别表学号姓名系名、系别表系名系主任、选课表学号课程号成绩。验证无损连接学生系别表和系别表的公共属性是系名系名→系主任成立无损选课表和学生系别表的公共属性是学号学号→姓名成立无损。验证范式三张表都消除了部分依赖和传递依赖达到 3NF。改造完之后再回头看那三种异常全消失了换系主任只改一行新建系直接插一行学生选课只影响选课表。这个过程我建议每个人都亲手做一遍比看十遍定义都管用。8. 复盘常见问题与排查技巧实录8.1 学习阶段的几个典型卡点第一个卡点是关系代数的除运算。教材上那个包含全部的语义绕来绕去我当时看了很久没懂。后来换了个理解方式除法对应的是对所有这类查询比如选修了全部课程的学生用 SQL 表达就是不存在一门课这个学生没选也就是双重 NOT EXISTS。抓住这个转换除运算就不用死记了。第二个卡点是范式的判定顺序。很多人一上来就判 3NF结果因为没先判 2NF 而判错。正确顺序永远是先求候选键再看非主属性然后判部分依赖2NF再判传递依赖3NF最后判决定因素是否包含码BCNF。这个流程走顺了题目再绕也不慌。第三个卡点是无损分解的表格法。教材上有个初始表格逐次改写的算法步骤多容易乱。我的做法是能用定理就用定理二元分解只有分解成三张以上表时才用表格法并且每一步都写下当前的表状态别在脑子里算。一个很实用的小技巧判范式之前先把所有函数依赖写成 X→Y 的形式列出来标出哪些属性出现在右边非主属性候选哪些从没出现在右边必然是候选键的一部分。这一步能省掉大量试错。8.2 实操阶段踩过的坑坑一NULL 导致的查询结果为空。写过WHERE grade 60想查不及格的结果 NULL 的行一条都没出来。原因是 NULL 参与比较的结果是未知不满足条件。正确写法是WHERE grade 60 OR grade IS NULL或者在建表时就该列设为 NOT NULL 加默认值。这个坑我在不同项目里见过至少三次。坑二GROUP BY 里漏了非聚合列。标准 SQL 要求 SELECT 列表里的非聚合列必须出现在 GROUP BY 中但有些数据库在宽松模式下会放行取一个不确定的值。这种查询在生产环境里是定时炸弹因为结果看起来有值但语义是错的。我的做法是开启严格模式让它在开发阶段就报错。坑三事务范围过大引发锁等待。有个批量处理的逻辑把几千条更新塞进一个事务还夹着一次外部调用结果高峰期大量请求排队。拆成小批次提交之后问题消失。这个教训是事务只包住必须原子化的那部分操作不要把网络 IO 和用户交互放进去。坑四外键约束和批量导入冲突。做数据迁移时先导子表后导父表外键直接报错。要么调整导入顺序要么临时禁用约束再恢复后者一定要在事务里做并且记得恢复否则后面全是脏数据。8.3 问题速查表现象可能原因排查方向查询结果比预期少条件里有 NULL 比较检查、、NOT IN与 NULL 的组合统计数字偏大多表连接产生重复行检查连接条件是否一对多放大考虑先去重再聚合更新后数据不一致冗余字段没有同步更新检查是否存在反范式设计补同步逻辑或改用视图插入报外键错误被参照记录不存在或顺序错误核对导入顺序检查外码取值并发下数字对不上隔离级别不足或事务范围过大检查是否读到了未提交数据缩小事务边界查询突然变慢索引失效或数据量增长看执行计划是否全表扫描检查条件列是否有索引分解后连接回去行数变多分解不是无损的用 R1∩R2 的定理重新验证分解条件这张表是我复习时用得最多的东西。考试前扫一遍能快速回忆起每个知识点的应用场景工作中遇到问题也能顺着现象往下找原因。它的价值不在答案而在把现象和理论对应起来的那条线。最后说一点个人体会。这门课最容易走偏的地方是把它当成一门记忆课——背定义、背范式、背协议。真正有效的学法是把每个概念都还原成一个具体的失败场景没有三级模式会怎样、没有范式会怎样、没有事务隔离会怎样。我后来带过一个同学复习他一开始也卡在范式上我让他别看定义先自己造一张有冗余的表然后手动模拟改一个系主任要改几行他做完这一步2NF 和 3NF 的区别自己就讲出来了。数据库这门东西理论是从事故里长出来的你顺着事故往回找比顺着定义往前推要快得多。