数据库笔记
关于数据库的笔记。
第一章:数据库系统概论
第一部分:数据模型(从现实到硬盘的抽象)
1. 概念模型
这是什么: 现实世界到信息世界的第一层抽象。它完全是为了让人看懂,不涉及任何具体的计算机技术。
常用工具: E-R图(实体-联系图)。比如,你要做一个教务系统,现实世界中有“学生”和“课程”,学生可以“选修”课程。在概念模型里,你只需要画出这三个框框和连线。
2. 逻辑模型
这是什么: 从信息世界到机器世界的抽象。它开始引入计算机的数据结构了,但依然屏蔽了底层的硬件细节。这是数据库管理员和开发者最关心的层面。
分类: 目录1.2中提到的层次模型(树状)、网状模型(图状)和关系模型(二维表)。我们现在用的MySQL、PostgreSQL基本都是关系模型。在这一步,之前的“学生”和“课程”概念,被具体化为了一张张有行有列的二维表格。
谁来做: “现实世界 => 概念模型 = 逻辑模型” 主要是由数据库设计人员来完成的。你需要去调研需求,画ER图,然后建表。
3. 物理模型
这是什么: 最底层,数据在硬盘上到底是怎么存的?是存成B+树还是哈希表?有没有建索引?
谁来做: 这部分通常由 DBMS(数据库管理系统) 自动完成。你只需要告诉MySQL“我要建一个表”,MySQL会自动决定如何在磁盘上分配空间。这就是为什么学完这门课后,你写的业务代码不需要关心硬盘长什么样。
第二部分:三级模式与两级映射
这是第一章的重中之重,也是考试和面试必考的难点。
为什么要搞这么复杂的架构?为了一个终极目标:数据独立性。 我们希望:底层硬件变了,上层代码不用改;数据库总表结构加了几个字段,上层应用代码也不用改。
1. 三级模式
内模式(内部层): 对应物理模型。它是数据在数据库内部的物理存储结构的描述。一个数据库只有一个内模式。
模式(概念层 / 逻辑模式): 对应逻辑模型。它是数据库中全体数据的逻辑结构和特征的描述。比如整个学校所有学生的完整信息表。一个数据库只有一个模式。
外模式(外部层 / 用户模式): 面向特定应用或用户的局部数据逻辑结构。比如,教务处看到的是所有学生的完整表(模式),但你登录系统时,只能看到自己的成绩表(外模式)。外模式可以有多个。
2. 两级映射
它们是实现“独立性”的关键。
映射1:外模式/模式映射
作用: 保证了逻辑数据独立性。
原理: 当全局的“模式”发生变化时(比如数据库管理员在学生表里偷偷加了一列“是否有对象”),只要修改这个映射关系,原本只查询“学号”和“成绩”的“外模式”就不会受到任何影响。你的前端应用一行代码都不用改,照样跑。
映射2:模式/内模式映射
作用: 保证了物理数据独立性。
原理: 当底层的存储结构发生变化时(比如为了提高速度,数据库管理员给“学号”这一列加了一个B+树索引,或者把机械硬盘换成了固态硬盘),只需要修改这个映射关系,全局的“模式”不变,所有的应用程序自然也不需要修改。
本章自测
假设你现在正在用C++或Java开发一个类似Steam的游戏商城应用。你突然发现首页加载游戏列表的速度太慢了,决定在数据库底层的“游戏名称”字段上加上索引以加快查询速度。
请问:
这个加索引的操作,改变的是三级模式中的哪一个模式?
为什么你的C++或Java后端查询代码完全不需要重写?请用“两级映射”的术语解释原因。
第二章:关系模型与关系代数
第一部分:关系模型基础
不要被“关系”这个高大上的词唬住了。在数据库里,“关系”就是一个二维表。
1. 数据结构
这里需要你把生活中的词汇“翻译”成学术词汇,考试全考这些术语:
- 关系: 一张二维表(例如一张“玩家角色表”)。
- 元组: 表里的一行数据(例如某个具体的玩家角色的所有信息)。
- 属性: 表里的一列(例如“角色名”、“等级”、“攻击力”)。
- 域: 某一列的取值范围(例如“等级”的域是 1~100 的整数)。
2. 数据完整性约束 - 核心考点
为了保证表里的数据不变成垃圾,数学模型规定了三条必须遵守的铁律:
- 实体完整性: 也就是主键规则。表里必须有一个属性能唯一标识一行数据,且绝对不能为 Null。比如玩家表里的 用户ID,不可能存在一个没有 ID 的幽灵玩家。
- 参照完整性: 也就是外键规则。这保证了表与表之间的引用是合法的。比如,“玩家背包表”里有一列 物品ID,那么这个 物品ID 必须在“游戏物品总表”里真实存在。你不能让玩家持有一个数据库里根本不存在的违规道具。
- 用户定义的完整性: 业务逻辑规则。比如,定义“充值金额”这一列绝对不能是负数。
第二部分:关系代数基础
我们在小学学过数字的代数($1 + 1 = 2$)。关系代数,就是把运算对象从“数字”变成了“二维表”。一个表加上另一个表,或者乘以另一个表,最后得到的结果依然是一个二维表。
这是本章最硬核的部分,分为两类运算:
1. 传统的集合运算
把表看作数学里的集合,也就是把一行一行的记录看成集合里的元素。
- 并 ($\cup$)、交 ($\cap$)、差 ($-$): 很好理解。比如表 A 是“近战干员”,表 B 是“法术干员”。A 交 B 就是既能近战又能用魔法的干员。前提是两张表的结构必须一模一样。
- 笛卡尔积 ($\times$): 最暴力的组合。假设表 A 有 3 行,表 B 有 4 行。A $\times$ B 就会把每一行都强行拼在一起,生成一张 $3 \times 4 = 12$ 行的大表。这是很多复杂查询的底层第一步,也是导致数据库卡死(内存爆炸)的罪魁祸首。
2. 专门的关系运算- 必须死磕的重点
这是 SQL 语句的数学理论基础,请务必分清以下三个符号:
选择 —— 符号 $\sigma$
- 原理:对表进行水平方向(行)的筛选。
- 大白话:挑出满足条件的记录。
- 例子:$\sigma_{等级 > 50}(玩家表)$,意思是从整个表里,把等级大于 50 级的那几行数据抽出来。
- 对应 SQL:
WHERE子句。
投影 —— 符号 $\pi$
- 原理:对表进行垂直方向(列)的筛选。
- 大白话:挑出你想要的字段,去掉不需要的列。
- 例子:$\pi_{角色名, 攻击力}(玩家表)$,意思是不管表里有多少列,我只要这两列组成一个新表。
- 对应 SQL:
SELECT 角色名, 攻击力。
连接 —— 符号 $\bowtie$
- 原理:把两张不同的表,根据某个共同的属性(通常是外键)拼接成一张大表。
- 大白话:找关系。比如把“用户基础信息表”和“用户排位战绩表”,通过 用户ID 这个共同列拼在一起,这样你就能在一个表里同时看到玩家的名字和他的胜率。
- 特点:这是关系型数据库最强大的地方,也是性能优化的重灾区。
第三部分:执行顺序与查询优化
就像练习一首极高难度的钢琴改编曲,把复杂的和弦与音符全部按对仅仅是基础;要想达到真正高水准的演奏,必须精确控制触键的先后顺序、力度和乐句的呼吸。数据库底层的关系代数也是一门极其讲究“执行节奏”的艺术。
正确的顺序(数据库查询优化器的核心原理)
在实际工程中,数据库引擎(或者优秀的开发者)会把“选择 (σ)”操作尽可能地提前,把“连接 (⋈)”操作尽可能地往后放。
最佳的推演步骤应该是这样的:
选择 绝对优先: 先单刷
Comments表,执行 σLikes>100(Comments)。 原理与收益:1000 万条评论中,点赞破百的可能只有 500 条。第一步我们直接把运算量从 1000 万级降维打击到了 500 级!连接: 拿这精简后的 500 条高赞评论,去和
Users表进行拼接 ⋈。 原理与收益:现在数据库只需要在Users表里找 500 次对应的用户 ID 即可,几乎是毫秒级瞬间完成,完全没有内存溢出的风险。投影 收尾: 最后执行 πUserName,Content,把不需要的
UserID、CommentID等多余的列砍掉,只返回干净的数据给前端。
用标准的数学符号连起来写,它的代数表达式是:
πUserName,Content(Users⋈UserID=UserID(σLikes>100(Comments)))
第四部分:核心术语深挖(连乘符号与码)
1. 为什么书上会有连乘符号 ∏?
我们在上一次交流中提到过,笛卡尔积是所有“连接”操作的底层基础,也是最容易导致数据库内存爆炸的操作。书上这个公式,就是对“内存爆炸”的严谨数学证明。
公式是这样的:
M=i=1∏nmi
原理解释: 这里的 ∏ 是数学里的连乘符号(和 ∑ 求和符号对应)。假设你有 n 个集合(也就是 n 个属性列),每个集合里有 mi 个不同的取值。把它们做笛卡尔积,最终生成的二维表会有多少行数据?答案是所有集合元素个数的乘积。
结合教材的例子:
D1(姓名集合)有 3 个人(m1=3)。
D2(课程集合)有 2 门课(m2=2)。
它们的笛卡尔积行数 M=m1×m2=3×2=6 行。
客观风险提醒: 这就解释了为什么我上次说先做连接是灾难。如果你有三张表,分别有 1万、10万、100万 行数据,如果不加任何条件直接做笛卡尔积,底层就会生成一张 104×105×106=1015 行的表。没有任何服务器能扛得住这种指数级的爆炸。所以,理解这个连乘符号,就是理解为什么要进行“查询优化”。
2. “码”这个概念重要吗?
极其重要。没有“码”,你的二维表就只是一张普通表格,根本不配称为“关系型数据库”。 对于以后要训练模型的你来说,数据的唯一性和干净程度决定了模型的生死:脏数据进去,脏结果出来。而“码”,就是数据库用来强制保证数据绝对唯一、不重复的底层规则。
书上(图3)列出了四个概念:超码、候选码、主码、外码。很多学生背完就忘,是因为没有理解它们之间“层层递进、不断精简”的逻辑关系。用大白话梳理一下这个选拔过程:
1. 超码 —— “能识别出你,哪怕带点废话”
定义: 只要能在一个表里唯一确定一行的属性(或属性组合),都叫超码。
例子: 在学生表里,
{学号}是超码;{学号, 姓名}也是超码;{学号, 姓名, 性别, 今天穿了什么颜色的袜子}还是超码。只要里面包含了能唯一识别你的东西,不管有多臃肿,都是超码。
2. 候选码 —— “没有任何废话的超码”
定义: 它是最精简的超码。你从超码里把多余的属性一个个剔除,直到再剔除任何一个,它就无法唯一识别一行数据了,这就叫候选码。
例子:
{学号, 姓名}不是候选码,因为踢掉姓名,光靠学号依然能识别。{学号}是候选码。同时,如果在你们学校里身份证号也是唯一的,那么{身份证号}也是一个候选码。一张表可以有多个候选码。
3. 主码 —— “天选之子”
定义: 既然有多个候选码(比如
学号和身份证号都能唯一识别),数据库设计者(也就是你)必须钦定其中一个作为这张表的老大。这个被选中的候选码,就是主码。约束: 主码绝对不能为空(这就是教材第一页学习指南里提到的“实体完整性”)。
4. 外码 —— “去别的表串门的通行证”
定义: 本表中的某列,是另一张表的主码。
例子: 你的“成绩表”里有一列叫
学生学号。这个学号在“成绩表”里不是主码,但它是“学生基础信息表”里的主码。那么学生学号就是“成绩表”的外码。没有外码,表与表之间就成了孤岛,无法进行我们上一节说的“连接”操作。
第五部分:候选码实战问答(严格监督)
1. 对局历史表(第一轮)
给出一个极其容易踩坑的实战问题:
场景: 你在为一个类似于“英雄联盟”的游戏设计数据库。现在有一张【对局历史表】,记录了所有玩家每一局的战绩。表里面有以下字段:
Player_ID(玩家唯一账号ID)Match_ID(对局唯一编号,比如这场游戏是全服第1000万场)Hero_Name(使用的英雄名称)Kills(击杀数)Deaths(死亡数)
请基于我们刚才梳理的定义,客观分析并回答: 在这张【对局历史表】中,候选码 应该是什么? (提示:一个玩家可以打很多场对局,一场对局里有10个不同的玩家。请仔细思考“唯一性”的构成。)
回答
Matchid可以作为候补码。但是一局游戏有适合玩家,也就是特定的这十个人开了一局,所以这十个playerid也可以作为候选码,对吗
解释
推导有两个致命的数据库设计误区。我必须非常严厉地指出:如果按照这个逻辑去建表,你的英雄联盟战绩查询系统在插入第二条数据时就会直接崩溃报错。
我们来彻底弄懂“码”的本质:
误区一:把 Match_ID 当作候选码
原理检验: 候选码的绝对铁律是唯一性,也就是在这一列里,同一个值绝对不能出现两次。
带入场景: 假设这是一场全服编号为
1000的排位赛。这局游戏里有 10 个玩家。在MatchHistory(对局历史表)中,为了记录这 10 个人的结算战绩(每个人使用的英雄、击杀、死亡都不一样),数据库里针对这一局游戏必须产生 10 行独立的数据。致命冲突: 这 10 行数据的
Match_ID字段全都是1000。既然1000重复出现了 10 次,它怎么可能唯一标识一行数据呢?一旦你把它设为码,数据库在存入这局游戏的第二个玩家战绩时,就会报出Duplicate entry '1000' for key 'PRIMARY'的严重错误。因此,单凭Match_ID绝对做不了候选码。
误区二:“特定的这十个 playerid 也可以作为候选码”
原理检验: 关系型数据库的“第一范式(1NF)”规定,表里的每一个属性(单元格)必须是“原子的”,也就是一个格子里只能存一个值。
带入场景: 你的
Player_ID这一列,在一行数据(一个元组)里只能写一个具体的账号 ID。你不可能在一个格子里塞进去 10 个人的 ID 作为一个整体。如果你指的是“把这 10 个人的 ID 分成 10 列组合起来”,这也违背了表设计的初衷。战绩表的一行就是“某一个玩家的单场表现”,绝不是“十个人的合影”。
1.1 标准解与原理:复合候选码
在关系型数据库中,要想找出候选码,你必须反问自己一个问题:“到底用什么字段的组合,才能像身份证号一样,绝对唯一地锁定这千万级数据量中的【某一行】?”
单看
Player_ID行不行?不行。一个玩家会玩成千上万局,Player_ID会无限重复。单看
Match_ID行不行?不行。一局游戏有 10 个人,Match_ID每局都要重复 10 次。
答案是,必须把它们两个绑在一起:{Player_ID, Match_ID} 的组合才是候选码。
- 逻辑证明: 在同一场极其特定的对局(例如
Match_ID = 1000)中,某个特定的玩家(例如Player_ID = Faker_T1)只会拥有一条结算记录。无论这两个单独拿出来怎么重复,但(Faker_T1, 1000)这个组合,在整个数据库的长河中是绝对唯一的,永远不可能出现第二次。
在数据库术语中,这被称为复合候选码。当我们钦定它作为这张表的核心标识时,它就是复合主键。
2. 购物车表(第二轮)
发现了吗?理解数据库,绝不能用日常感性的思维去“想当然”,必须严格遵循数学集合里的“唯一性”约束。这也是很多新手在实际项目中导致数据错乱的根源。
为了确保你真正掌握了这个核心逻辑,并能应用到你目前正在做的 Web 项目中,我们换一个电商前端开发经常会碰到的后端表设计场景:
场景: 你正在用 Vue 开发一个个人网站的商城模块,现在需要设计一张底层的【购物车表】。 表里有三个核心字段:
User_ID(买家账号ID)Product_ID(商品编号)Quantity(加购的数量)
请问:这张【购物车表】的候选码是什么?为什么? 请像我刚才分析英雄联盟战绩表那样,用“唯一性”和“防重复”的逻辑,给我一个严谨的理论证明。
回答
那候选码就是谁买了什么买了多少件三个合一起,这是唯一且无法重复的,因为什么东西买了多少,谁买了多少件东西都是有概率重复的,任意选两个都是有概率重复的。
解释
你的推导再次踩中了数据库设计中最隐蔽的陷阱:把“可变状态(属性)”当成了“身份标识(坐标)”。 这不仅违背了候选码“最小化(无废话)”的数学定义,在实际工程中更会引发灾难级的业务 Bug。我们来客观剖析一下这个设计的风险,以及它为什么在原理上是不成立的。
2.1 风险评估:为什么不能加上 Quantity?
当你开发校园跑腿或外卖小程序的购物车模块时,思考一下真实的业务逻辑: 如果用户(User_ID: 001)把一份黄焖鸡(Product_ID: 101)加入了购物车,数量是 1。过了一会,他又点了一下“+1”。
如果按你的设计(候选码 =
{User_ID, Product_ID, Quantity}): 数据库会认为(001, 101, 1)和(001, 101, 2)是两个完全不同、合法的组合。结果就是,数据库里插入了两行记录。用户的购物车列表里会出现两行黄焖鸡,而不是把原来的那一行数量变成 2。这完全违背了购物车的合并逻辑。底层性能灾难: 主码/候选码在数据库底层是用来构建索引树(如 B+ 树)的骨架,它是极难被修改的。如果你把
Quantity放在候选码里,每次用户点击购物车里的“+1”或“-1”,底层就无法使用简单的UPDATE语句来修改数量,而是被迫执行高代价的DELETE(删除旧码)然后再INSERT(插入新码)。当并发量大时,数据库会直接锁死崩溃。
2.2 严谨证明:正确的候选码是什么?
正确的候选码仅仅是:{User_ID, Product_ID}。
用图3教材上的定义来做严密的证明:
唯一性: “谁”加上“什么商品”,已经足够唯一定位购物车里的某一行记录了。张三的购物车里,“黄焖鸡”这一项永远只会占据唯一的一行。
最小性(也就是剔除废话): 教材上明确指出,候选码的任何真子集都不能是超码。
Quantity(数量)是依附于这行记录上的一个会变化的属性,它就像是这行数据的“血条”或者“蓝量”。用来定位玩家坐标的是 ID,而不是他的血条。既然{User_ID, Product_ID}已经能唯一确定一行了,再加上Quantity就属于画蛇添足,它退化成了“超码”,不再是“候选码”。
3. 考试成绩表(第三轮)
数据库的基石如果打不稳,未来面对海量数据进行数据清洗或投喂给 AI 模型时,全都会因为冗余和错乱而崩塌。你的思维惯性在于总是试图把所有相关字段都塞进主键里。
为了彻底拔除你对“最小性”的误解,我们进行最后一次极其刁钻的理论检验。请剥离感性,完全用严谨的数学逻辑来回答:
场景: 学校的【考试成绩表】。字段有: StudentNo (学号) CourseNo (课程号) ExamDate (考试日期) Score (分数)
特殊业务规则: 你们学校允许学生对同一门课进行多次补考或刷分,并且教务系统需要把每一次的考试记录都保留下来,不能覆盖。
请问:在这个极其特定的规则下,这张表的候选码是什么? (仔细想清楚“唯一性”和“最小性”,推演一下如果你少选一个字段会报什么错,多选一个字段又为什么是废话。)
回答
StudentNo + CourseNo + ExamDate,少一个字段(课程号)的话,会因为可以刷分,而认为不是唯一的,少了其他的会发生,同一天很多学生考这一门,或学生一天考多门的情况。如果多了分数的话,因为分数是被确定的数据,不是用来确定字段的数据,不违反唯一但是违反最小。如此对吗
解释
🎉 完美!你彻底开悟了!这次的逻辑推演堪称教科书级别。
面对我连续高强度的刁钻提问,你不仅没有被绕进去,反而极其精准地抓住了底层数学逻辑。既然你要求我严格把关你的学习质量,那我现在可以客观且负责任地告诉你:你真正吃透了“关系模型”和“码”的核心本质。
我们来复盘一下你刚才这段极其精彩的推导,把你脑海中的直觉彻底固化为理论武器:
3.1 复盘:为什么你的推导是满分?
1. 洞悉了“最小性”的灵魂(关于分数的分析) 你对 Score(分数)的判断一针见血:“分数是被确定的数据……不违反唯一但是违反最小”。这句话直接击穿了知识点!你终于把用来定位的“坐标系”和挂在坐标系上的“属性值”严格区分开了。
一旦加上了分数,它就退化成了超码;踢掉分数,它才是最干净利落的候选码。
2. 严密的唯一性反证(关于少字段的分析) 你的反证逻辑非常清晰。我再稍微帮你补充一个极致的细节场景:
如果你漏掉了 CourseNo,把候选码定为 {StudentNo, ExamDate},这意味着在数据库的视角里,“一个学生在同一天只能有一条记录”。如果这个学生上午考了《数据库》,下午又考了《操作系统》,当教务员录入下午的成绩时,底层数据库就会直接报错(主键冲突)。因此,必须加上课程号才能锁定那场特定的考试。
第三章:SQL
这一章开始,你就正式从“数据库理论玩家”切换成“数据库指令操作者”了。
前两章讲的是:数据库为什么这样设计。 这一章讲的是:你到底怎么和数据库说话。
SQL 的本质非常简单粗暴:你不需要告诉数据库“怎么一步一步做”,你只需要告诉它“我要什么结果”。 至于底层怎么找、怎么连、怎么筛、怎么排序,那是数据库优化器的活。
第一部分:SQL 概述
1. SQL 是什么?
- 全称: Structured Query Language,结构化查询语言。
- 作用: 用来定义数据库、操作数据库、查询数据库、控制数据库。
- 地位: 它是关系数据库的标准语言。MySQL、PostgreSQL、SQL Server、Oracle 虽然方言略有差异,但核心思想几乎一致。
你可以把 SQL 理解成数据库世界里的“普通话”。
2. SQL 的发展
教材里讲“SQL 发展”,本质上不是让你背年份,而是让你知道:它是从关系代数和关系演算的理论里长出来的工业语言。
- 最早的关系数据库理论由 Codd 提出。
- 后来 IBM 把理论工程化,搞出了早期的 SQL 原型。
- 再后来 ANSI / ISO 把它标准化,于是它变成了全世界数据库都要兼容的通用语言。
所以,SQL 不是某个厂商瞎发明的私货,而是关系型数据库的“通用操作体系”。
3. SQL 的特点
这部分很容易考简答题,必须能说出人话:
- 综合统一: 一门语言同时干四件事,建库、建表、查数据、控权限都能做。
- 高度非过程化: 你只描述“要什么”,不描述“怎么做”。例如你写
SELECT * FROM Student WHERE Age > 20,并不用规定先扫哪张索引、先读哪一页磁盘。 - 面向集合操作: SQL 一次处理的是一批行,不是像 C 语言那样一条条手写循环。
- 使用方式灵活: 既能交互式敲命令,也能嵌入 Java、C++、Python 代码里。
- 语言简洁但功能很强: 看起来就那几个关键字,实际上从简单查询到复杂分析都能覆盖。
4. SQL 查询的基本概念
第三章的大头是“查询”,而查询的底层骨架其实就一句话:
$$ ext{SELECT} \rightarrow \text{FROM} \rightarrow \text{WHERE} \rightarrow \text{GROUP BY} \rightarrow \text{HAVING} \rightarrow \text{ORDER BY} $$
别急着背,先理解每个部件在干什么:
FROM:先告诉数据库你从哪几张表里拿原材料。WHERE:先筛行。GROUP BY:再按某种维度分组。HAVING:对分组结果继续筛选。SELECT:最后决定输出哪些列。ORDER BY:最后给结果排个顺序。
你会发现,它和第二章的“选择、投影、连接、排序”其实是一一对应的。
第二部分:单表查询
单表查询看起来最简单,但它其实已经把 SQL 的基本功全包了:投影、选择、排序、聚合,都在这里。
假设有一张学生表:
1 | Student( |
1. 投影运算:挑列
如果你只想看学生姓名和系别,而不关心年龄、奖学金等信息:
1 | SELECT Sname, Sdept |
- 本质: 这是关系代数里的投影 $\pi$。
- 作用: 从一整张表中,抽取你关心的列。
- 风险提醒:
SELECT *虽然爽,但工程里常常是坏习惯。因为你可能把完全用不到的列也查出来,浪费 IO、网络带宽和前端处理成本。
2. 选择运算:挑行
如果你只想看计算机系且年龄大于 20 的学生:
1 | SELECT * |
- 本质: 这是关系代数里的选择 $\sigma$。
- 作用: 只保留满足条件的元组。
- 常见条件: 比较运算符、逻辑运算符、区间判断、空值判断、模糊匹配。
例如:
1 | SELECT Sname |
3. 排序运算:让结果有秩序
如果你想按奖学金从高到低看名单:
1 | SELECT Sname, Scholarship |
ASC是升序,默认可不写。DESC是降序。
客观提醒: 如果你没有写 ORDER BY,那数据库返回的行顺序在理论上是不保证稳定的。别把“这次看起来顺眼”误认为“永远都这样”。
4. 查询表:把结果当成一张新表
教材里的“查询表”可以理解成:一条查询语句的结果,本身也可以继续被当成表来用。
比如你先找出高奖学金学生,再在外层继续处理:
1 | SELECT T.Sname |
这就是“查询结果表”。很多复杂 SQL,都是把中间结果一层层包出来的。
5. 聚合查询:不要只看单条记录,要看整体规律
聚合函数常考五个:
COUNT():统计个数SUM():求和AVG():平均值MAX():最大值MIN():最小值
例子:统计计算机系学生人数。
1 | SELECT COUNT(*) AS total |
例子:按系统计平均年龄。
1 | SELECT Sdept, AVG(Sage) AS avg_age |
如果你还想筛掉平均年龄不超过 20 的系:
1 | SELECT Sdept, AVG(Sage) AS avg_age |
这里非常容易混淆:
WHERE是分组前筛选行。HAVING是分组后筛选组。
这两个概念要是混了,考试会直接送分给别人。
第三部分:连接查询
当数据分散在多张表中时,单表查询就不够用了。真正体现关系型数据库威力的,是连接查询。
假设有两张表:
1 | Student(Sno, Sname, Sdept) |
Student 存学生基本信息,SC 存选课成绩。
1. 等值连接与非等值连接
等值连接最常见,就是用相等条件把两表接起来:
1 | SELECT Student.Sname, SC.Cno, SC.Grade |
或者写成更现代、更清晰的形式:
1 | SELECT s.Sname, sc.Cno, sc.Grade |
非等值连接则不是用等号,而是用范围或大小关系连接。
例如有一个成绩等级表:
1 | GradeLevel(LowScore, HighScore, LevelName) |
你想把学生成绩映射成“优秀、良好、及格”:
1 | SELECT sc.Sno, sc.Grade, gl.LevelName |
这就是非等值连接。
2. 自连接
自连接的意思是:同一张表和自己连。
最常见场景是“员工表”里同时存员工和上级:
1 | Employee(Eid, Ename, ManagerId) |
查询“员工及其上级姓名”:
1 | SELECT e.Ename AS employee_name, m.Ename AS manager_name |
这里必须起别名,不然数据库根本分不清你在说“这张表里的谁”。
3. 外连接
内连接的逻辑是:两边都匹配上了才保留。
但很多业务里,你恰恰想保留“没匹配上的那部分”。比如查询“所有学生及其选课情况”,哪怕有些学生压根没选课,也要显示出来。
1 | SELECT s.Sname, sc.Cno, sc.Grade |
LEFT JOIN:保留左表全部数据。RIGHT JOIN:保留右表全部数据。FULL OUTER JOIN:两边都保留。不是所有数据库都直接支持。
理解重点: 外连接的意义,不是“更高级”,而是处理缺失关系。现实世界的数据永远不完美,所以外连接很常用。
第四部分:嵌套子查询
子查询,就是“在一条 SQL 里面再塞一条 SQL”。
它本质上是:先用内层查出一个结果,再把这个结果交给外层继续判断。
1. 使用 IN 的子查询
例子:查询选修了课程 C001 的所有学生姓名。
1 | SELECT Sname |
内层先查出所有选修 C001 的学号,外层再去 Student 表里按这些学号找人。
2. 使用集合比较运算符的子查询
常见集合比较有 > ALL、> ANY、< ALL 等。
例子:查询成绩高于课程 C002 所有成绩的学生记录。
1 | SELECT Sno, Cno, Grade |
解释一下:
> ALL:比子查询结果中的每一个值都大。> ANY:只要比其中某一个值大即可。
这类题的坑在于:你到底是在和“一个值”比,还是和“一组值”比。 一旦看错,整题逻辑就会反。
3. 使用 EXISTS 的子查询
EXISTS 不关心子查询具体返回什么值,它只关心一件事:子查询有没有结果。
例子:查询至少选了一门课的学生。
1 | SELECT s.Sname |
这类写法在“存在性判断”时非常强,尤其适合表达“有没有”“是否存在关联记录”这种业务语义。
4. 复杂子查询实例
这类题通常长得吓人,但拆开看其实就三步:
- 先确定最内层在找谁。
- 再确定中间层如何过滤。
- 最后看外层到底要输出什么。
例如:查询“选修了和张三同样全部课程的学生”。
这类题通常需要双重否定、嵌套 NOT EXISTS 才能写出来。它难的不是语法,而是逻辑量词:你能不能把“全部”翻译成数据库能执行的条件。
所以面对复杂子查询,你不要慌,记住一句话:先翻译逻辑,再翻译 SQL。
第五部分:集合运算
关系代数里有并、交、差,SQL 里也有对应操作。
1. 并集
1 | SELECT Sno |
表示选了 C001 或 C002 的学生学号。
2. 交集
某些数据库支持 INTERSECT:
1 | SELECT Sno |
表示两门课都选了的学生。
3. 差集
某些数据库支持 EXCEPT 或 MINUS:
1 | SELECT Sno |
表示选了 C001 但没选 C002 的学生。
考试提醒: 并、交、差的前提和第二章一样,参与运算的结果集必须“可相容”,也就是列数和对应数据类型得能对上。
第六部分:SQL 查询的一般格式
这部分看似最水,实际上是整章的骨架。你把这套骨架背清楚,很多题都能现推出来。
1 | SELECT [DISTINCT] 目标列 |
逐条解释:
SELECT:要哪些列。DISTINCT:去重。FROM:从哪些表来。WHERE:先过滤行。GROUP BY:再分组。HAVING:再筛组。ORDER BY:最后排序。
很多人写 SQL 乱,是因为脑子里没有这个模板。你以后写题,先把这个框架默写出来,再往里填条件,错误率会大幅下降。
第七部分:SQL 中的数据定义基础
1. 基本格式
1 | CREATE TABLE 表名 ( |
2. 常见数据类型
CHAR(n):定长字符串。VARCHAR(n):变长字符串。NUMBER(n):定长数字型,教材里常见于 Oracle 风格。INT:长整型。SMALLINT:短整型。BIGINT:大整型。FLOAT(n):浮点数。DATE:日期。TIME:时间。
3. 列级完整性约束
PRIMARY KEY:主键。单列主键时可以直接写在列后。NOT NULL:该列不允许为空。UNIQUE:该列值必须唯一,但和主键不同,它通常表示“候选码中的其他备选唯一列”。CHECK(条件):检查这一列是否满足某种规则。
例如:
1 | CREATE TABLE Student ( |
4. 表级完整性约束
当主键由多个属性构成时,必须写成表级约束:
1 | CREATE TABLE SC ( |
外键也是表级约束的高频考点:
1 | CREATE TABLE SC ( |
这一步你就能看到第二章那些抽象概念,如何在 SQL 里真正变成硬约束了。
本章自测
场景: 学校数据库中有三张表:
Student(Sno, Sname, Sdept)Course(Cno, Cname, Credit)SC(Sno, Cno, Grade)
请回答:
- 如果你要查询“所有学生姓名及其成绩,即使有学生还没选课也要显示出来”,应该用内连接还是外连接?为什么?
- 如果你要统计每个系的平均成绩,并且只保留平均成绩大于 80 的系,为什么筛选条件不能写在
WHERE后面,而应该写在HAVING后面? - 为什么复合主键
(Sno, Cno)必须写成表级约束,而不能简单理解成两个单独的PRIMARY KEY?
第四章:数据库建模
从这一章开始,你的身份又变了。
第三章里你是在学“怎么查表”;第四章里你是在学“这张表一开始应该怎么设计”。
这章如果没学透,后面写 SQL 再熟练也只是“在一个错误的地基上疯狂装修”。数据库建模的本质,就是在真正建表之前,先把现实世界的对象、关系、约束,用一种严谨的方式抽象出来。
第一部分:数据库设计过程
数据库设计不是灵感创作,而是一套分层推进的工程流程。
1. 基本流程
通常可以分成以下几个阶段:
- 需求分析:先搞清楚系统到底要管什么数据。
- 概念设计:画 E-R 图,把实体、属性、联系理顺。
- 逻辑设计:把 E-R 模型转成关系模型,也就是一张张表。
- 物理设计:考虑索引、存储、性能优化。
- 实施与维护:真正建库、导数据、上线、迭代。
考试里问“数据库设计过程”,你千万别只答“画 E-R 图”。E-R 图只是其中一层,不是全部。
2. 为什么必须先建模,再写 SQL?
因为 SQL 只是操作工具,而建模决定的是:
- 你的表会不会冗余
- 主键设计是否合理
- 外键关系是否清晰
- 后期查询是不是一团乱麻
说白了,错误的模型会把所有后续开发全部污染。
第二部分:E-R 模型基本概念及表示
1. 实体与实体集
- 实体: 客观存在并且可以相互区分的事物。
- 实体集: 同一类实体的集合。
比如在大学选课系统里:
- “张三”是一个实体。
- “所有学生”构成一个实体集。
- “数据库原理”这门课是一个实体。
- “所有课程”构成另一个实体集。
不要把“学生”理解成某一个人。教材里的“学生”通常指的是学生实体集。
2. 属性
属性是实体的特征。
例如学生实体可以有:
- 学号
- 姓名
- 性别
- 年龄
- 所属院系
属性还有一些细分:
- 简单属性 / 复合属性:如“地址”可以拆成省、市、街道。
- 单值属性 / 多值属性:如“联系电话”可能有多个。
- 存储属性 / 派生属性:如“年龄”可以由出生日期推导出来。
考试有时会问你:为什么“年龄”不适合直接长期存储?因为它会变,而“出生日期”更稳定。
3. 联系与联系集
- 联系: 实体之间的关联。
- 联系集: 同一类联系的集合。
例如:
- 学生 与 课程 之间有“选修”联系。
- 教师 与 课程 之间有“授课”联系。
这里的“选修”不是一个动作词而已,它在建模里是一个严肃的结构对象。
第三部分:约束
这一部分是第四章的硬骨头。E-R 图不是随便画几个框和线,关键在于你得把现实世界的限制条件也表达进去。
1. 映射约束
映射约束描述的是联系双方的数量对应关系,也就是你熟悉的:
- 1:1
- 1:n
- m:n
例如:
- 一个班级只有一个班长,一个学生最多担任一个班长职位,这接近
1:1。 - 一个院系有很多学生,一个学生只属于一个院系,这是
1:n。 - 一个学生可以选很多门课,一门课也能被很多学生选,这是
m:n。
这决定了你未来建表时到底是“在哪边放外键”,还是“必须单独拆一张联系表”。
2. 码约束与联系集的属性安置
这块非常容易乱。
先说结论:如果一个联系本身有自己的属性,那这个联系通常不能直接当一根简单的线,它往往要被认真对待,甚至独立转成关系模式。
例如“学生 选修 课程”这个联系,本身就可能带有:
- 选课时间
- 成绩
- 重修标记
这些属性既不属于学生实体本身,也不属于课程实体本身,而是属于“学生和课程之间那次具体的联系”。
所以,Grade 不该塞进 Student 表,也不该塞进 Course 表,而应该放进联系对应的表,例如 SC(Sno, Cno, Grade)。
这就是“联系集属性安置”的核心思想。
所谓码约束,本质上是在问:这个联系里,谁能唯一标识一条联系记录?
例如在 SC 中,通常是 (Sno, Cno) 来唯一标识一次“学生选了某门课”的联系。
3. 依赖约束
依赖约束可以理解为:某个实体或联系的存在,是否依赖于另一个实体。
比如“订单明细”离开“订单”本身就没有意义;“成绩记录”离开“学生”和“课程”也失去存在基础。
这类约束在后面会直接影响你如何设计外键,以及是否需要级联删除。
4. 参与约束
参与约束描述的是:某个实体集中的实体,是否必须参与某种联系。
- 全参与:每个实体都必须参加。
- 部分参与:允许有些实体不参加。
例如:
- 如果规定“每门课都必须有教师授课”,那么课程在“授课”联系中是全参与。
- 如果不是每个学生都已经选课,那学生在“选修”联系中就是部分参与。
这类约束会影响你对业务规则的理解。它不是画图细节,而是系统规则本身。
5. 多值联系
多值联系的意思是:某种联系不只是两个实体之间的,而是三个或更多实体一起参与。
例如一个“供应”场景:
- 供应商
- 零件
- 工程项目
你要表达的是“某供应商给某项目供应某零件”,这就是一个三元联系。
这类联系不能简单拆成若干个二元联系,否则语义会丢失。
因为:
- “供应商 A 和零件 X 有关系”
- “项目 P 和零件 X 有关系”
并不等价于“供应商 A 给项目 P 供应零件 X”。
这就是为什么教材专门强调“多值联系的建模”。
第四部分:弱实体集
弱实体是第四章很容易考定义题的点。
1. 什么是弱实体?
弱实体的特点是:它自己没有足够的属性来唯一标识自己,必须依赖别的实体才能存在和被识别。
例如:
Order是订单。OrderItem是订单明细。
如果只看“第 1 行明细”“第 2 行明细”,它们根本没法脱离订单独立存在。必须加上“属于哪个订单”,才能唯一确定。
所以:
- 订单是强实体。
- 订单明细是弱实体。
2. 弱实体为什么重要?
因为它会强迫你承认一个事实:不是所有对象都能靠自己单独活着。
建模时一旦把弱实体误画成强实体,后面主键设计和外键依赖往往都会错。
第五部分:扩展 E-R 特征
基础 E-R 模型已经很强了,但现实世界更复杂,所以教材又引入了扩展 E-R 模型。
1. 类层次
类层次就是“泛化 / 继承”的思想。
例如:
- 人员
- 学生
- 教师
它表示“学生”和“教师”都继承自更一般的“人员”。
这样建模的好处是:
- 公共属性放在父类,比如姓名、性别。
- 特有属性放在子类,比如学生有专业,教师有职称。
这可以减少重复设计。
2. 聚合
聚合是一个很抽象但很有用的东西。你可以把它理解成:把一整个联系,当成更高层次里的一个整体对象。
例如:
- 教师 “指导” 学生完成毕业设计。
- 现在你还想表达“院系 对这个指导关系进行审核”。
这时你不是简单地连几个点,而是要把“指导”这个联系整体拎出来,再和“院系”发生新联系。这就是聚合。
如果没有聚合,很多“关系上的关系”根本无法严谨表达。
第六部分:E-R 建模问题
这一节专门解决一个现实问题:你知道概念,但真正画图时,还是经常不知道“这个东西到底该画成实体、属性,还是联系”。
1. E-R 建模的基本原则
记住三条铁律:
- 稳定对象优先建成实体。
- 依附于对象的描述信息优先建成属性。
- 对象之间的交互关系优先建成联系。
举例:
- “学生”是实体。
- “学生姓名”是属性。
- “学生选修课程”是联系。
如果你把“选修”画成属性,基本就说明思路已经崩了。
2. 依赖约束的建模
当一个对象离开另一个对象就失去存在意义时,你要高度警惕:这可能是弱实体,也可能意味着必须存在强依赖外键。
比如“宿舍床位”如果系统规定它一定依附于具体宿舍,那它就不该被建成完全独立的自由实体。
3. 多值联系的建模
三元及以上联系不要硬拆。
因为一旦你把一个三元联系强行拆成三个二元联系,原本的“联合语义”就可能消失。数据库里最怕的不是表多,而是语义错。
所以建模时要问自己:这到底是三件两两相关的事,还是一件必须三方同时成立的事?
第七部分:数据库概念设计实例 - 大学选课系统
这部分通常是整章综合题。你真正要学的不是“背图”,而是学会推导。
1. 概念设计任务
要设计一个大学选课系统,系统至少要管理:
- 学生
- 课程
- 教师
- 院系
- 选课记录
2. 系统需求分析
假设需求如下:
- 每个学生属于一个院系。
- 每门课程可由某位教师授课。
- 一个学生可以选多门课。
- 一门课可以被多个学生选。
- 每次选课记录要保存成绩。
从这一步你就能提炼出:
- 实体集:学生、课程、教师、院系
- 联系集:属于、授课、选修
3. 数据库概念设计
概念层面可以抽象成:
Student(Sno, Sname, SdeptNo)Department(DeptNo, DeptName)Teacher(Tno, Tname)Course(Cno, Cname, Credit)SC(Sno, Cno, Grade)作为“选修”联系的实现载体
其中最关键的一步是看出:
- “学生 - 课程”是
m:n - 并且这个联系带有
Grade - 所以必须独立出
SC
如果你把成绩直接塞进学生表或课程表,这个模型就已经坏了。
第八部分:逻辑设计 - E-R 模型转化为关系模型
这是第四章的落地环节,也最贴近真正建表。
1. E-R 模型转换方法
常见转换规则如下:
- 一个实体集通常转成一张关系表。
- 1:1 联系:可以在任意一方加入外键,视业务而定。
- 1:n 联系:通常把
1端主键作为外键放到n端。 - m:n 联系:必须单独新建一张关系表,主键通常由两边主键组合而成。
- 联系若带有属性:这些属性放到表示该联系的关系表里。
- 弱实体:关系表主键通常包含其依赖的强实体主键。
2. 大学选课系统转换实例
把刚才的 E-R 模型转换后,可以得到:
1 | Student(Sno, Sname, DeptNo) |
其中:
Student.DeptNo是外键,指向Department(DeptNo)。Course.Tno是外键,指向Teacher(Tno)。SC(Sno, Cno, Grade)表示学生与课程之间的m:n联系。SC的主键通常是(Sno, Cno)。
这一步实际上就是把第四章的“图”,翻译成第三章可以执行的“表结构”。
本章自测
场景: 你要为一个校园竞赛报名系统做建模。需求如下:
- 学生可以报名多个比赛。
- 每个比赛可以有多个学生报名。
- 每条报名记录都必须保存“报名时间”和“审核状态”。
- 每个比赛由一个学院负责。
请回答:
- “学生”和“比赛”之间是什么映射约束?
- 为什么“报名”不能只画成一条普通连线,而应该被认真建模成带属性的联系,甚至在逻辑设计时转成单独的表?
- 在关系模型里,这张“报名表”的主键最自然的设计应该是什么?为什么“审核状态”不能放进主键里?
第三章与第四章串联总结
如果你把这两章打通,你会突然发现数据库这门课的主线其实特别清晰:
- 第二章 告诉你什么叫关系、什么叫码、什么叫关系代数。
- 第三章 告诉你如何用 SQL 去操作这些关系。
- 第四章 告诉你这些关系一开始应该怎么设计出来。
也就是说:
- 第二章是理论骨架。
- 第三章是操作语言。
- 第四章是建模方法。
真正学会数据库,不是会背 SELECT,也不是会画两个矩形框,而是你能在脑子里打通这条完整链路:
现实世界需求 -> E-R 建模 -> 关系模型 -> SQL 操作。
第五章:关系数据库理论与规范化
前四章我们一直在解决“表是什么、怎么查、怎么从现实世界建出来”。但这里还有一个更危险的问题:
表建出来以后,它会不会越用越脏?
如果一张表里同时塞了学生信息、课程信息、成绩信息、院系信息,看起来一开始很方便,后面就会像把所有衣服、充电线、证件、零食都塞进同一个抽屉:找东西痛苦,改东西痛苦,删东西更痛苦。
关系数据库理论与规范化,就是在教你整理这个抽屉。它不只是背 1NF、2NF、3NF,而是要你看懂:哪些字段应该待在一起,哪些字段必须拆出去。
第一部分:函数依赖
1. 什么是函数依赖
在关系模式 R(U) 中,如果属性集 X 的值一旦确定,就能唯一确定属性集 Y 的值,那么称:
$$ X \rightarrow Y $$
读作:X 函数决定 Y。
大白话:X 是钥匙,Y 是这把钥匙打开后必然能拿到的信息。
例如:
学号 -> 姓名:一个学号只能对应一个学生姓名。课程号 -> 课程名:一个课程号只能对应一门课程。(学号, 课程号) -> 成绩:某学生选某门课后,才有一个对应成绩。
千万别把“看起来相关”当成函数依赖。函数依赖不是“我感觉它们有关”,而是“只要钥匙一样,柜子里拿出来的东西就必须一样”。
2. 平凡依赖与非平凡依赖
如果 Y 本来就是 X 的一部分,那么 X -> Y 叫平凡函数依赖。
例如:
1 | (学号, 课程号) -> 学号 |
这没什么信息量,因为左边本来就包含学号。
如果 Y 不是 X 的子集,就叫非平凡函数依赖。真正有信息量的是非平凡依赖。
3. 完全函数依赖与部分函数依赖
如果 X -> Y,并且 X 的任何真子集都不能推出 Y,那么 Y 完全函数依赖于 X。
例如:
1 | (Sno, Cno) -> Grade |
如果只靠 Sno 推不出成绩,只靠 Cno 也推不出成绩,那么这是完全函数依赖。
如果 X 的某个真子集已经能推出 Y,就是部分函数依赖。
例如:
1 | (Sno, Cno) -> Sname |
既然只靠 Sno 已经能推出 Sname,那 Sname 对 (Sno, Cno) 就是部分依赖。
部分依赖是 2NF 的敌人。它的坏处是:一把复合钥匙明明有两齿,但某个信息只认其中一齿,说明这个信息其实不该放在这张大表里。
4. 传递函数依赖
如果:
1 | X -> Y |
并且 Y 不能反过来决定 X,那么 Z 对 X 存在传递依赖。
例如:
1 | Sno -> DeptNo |
于是:
1 | Sno -> DeptName |
DeptName 就是通过 DeptNo 间接被 Sno 决定的。
传递依赖是 3NF 的敌人。它像传话:你本来想通过学号找到学生信息,结果还绕了一层院系编号再找到院系名。绕得越多,冗余和修改错误就越容易冒出来。
第二部分:属性闭包与候选码
只要题目给你:
1 | R(A, B, C, D, E, F) |
然后问候选码,你就不要凭感觉猜。候选码不是拍脑袋选出来的,必须用闭包算。
1. 属性闭包是什么
属性集 X 关于函数依赖集 F 的闭包,记作:
$$ X^+ $$
意思是:从 X 这串钥匙出发,根据已有函数依赖,最多能打开哪些柜子、拿到哪些信息。
如果:
$$ X^+ = U $$
说明 X 能推出关系模式里的全部属性,那么 X 是超码。
如果 X 是超码,并且再删掉任何一个属性都不再是超码,那么 X 是候选码。
2. 闭包计算步骤
闭包题固定按这四步走:
- 先把自己放进去:
X+ = X。 - 扫描函数依赖集,凡是左边已经包含在
X+里的,就把右边加入X+。 - 反复扫描,直到再也加不进新属性。
- 如果最后得到全集
U,说明X是超码;再检验最小性,判断是不是候选码。
3. 试卷例型:求候选码
题型:
1 | R(A, B, C, D, E, F) |
先找一个非常有用的技巧:
没有出现在任何依赖右边的属性,一定必须出现在候选码里。
右边出现过:D, B, E, F。
没有出现在右边:A, C。
所以候选码至少包含 A, C。
计算 (AC)+:
1 | 初始:(AC)+ = {A, C} |
所以 AC 是超码。
再看最小性:
A+ = {A, B, F},不是全集。C+ = {C, D},不是全集。
因此:
1 | 候选码 = AC |
这类题最稳的写法就是:先找必含属性,再算闭包,再检验最小性。
第三部分:范式
范式的目标只有一个:减少冗余,避免插入异常、删除异常、修改异常。
1. 第一范式 1NF
每个属性值必须是原子的,不能在一个格子里塞一串值。
错误例子:
1 | Student(Sno, Sname, Phones) |
正确做法:把多值属性拆成单独表。
1 | Student(Sno, Sname) |
2. 第二范式 2NF
前提:已经满足 1NF。
要求:非主属性必须完全函数依赖于候选码,不能部分依赖。
典型坏表:
1 | SC(Sno, Cno, Sname, Cname, Grade) |
依赖关系:
1 | Sno -> Sname |
Sname 只依赖候选码的一部分 Sno,Cname 只依赖候选码的一部分 Cno,所以违反 2NF。
拆分:
1 | Student(Sno, Sname) |
3. 第三范式 3NF
前提:已经满足 2NF。
要求:非主属性不能传递依赖于候选码。
典型坏表:
1 | Student(Sno, Sname, DeptNo, DeptName) |
依赖:
1 | Sno -> DeptNo |
所以:
1 | Sno -> DeptName |
这是传递依赖,违反 3NF。
拆分:
1 | Student(Sno, Sname, DeptNo) |
4. BCNF
BCNF 比 3NF 更严格。
对于每一个非平凡函数依赖:
1 | X -> Y |
都必须满足:X 是超码。
考试判断 BCNF 的流程:
- 先求候选码。
- 对每条函数依赖
X -> Y,计算或判断X是否为超码。 - 只要有一条依赖的左边不是超码,就不是 BCNF。
继续用刚才的例题:
1 | R(A, B, C, D, E, F) |
逐条判断:
C -> D:C不是超码,违反 BCNF。A -> B:A不是超码,违反 BCNF。AC -> E:AC是候选码,满足 BCNF。B -> F:B不是超码,违反 BCNF。
所以这个关系模式不是 BCNF。
再判断 3NF:
3NF 对每条 X -> A 要求满足以下至少一个:
X -> A是平凡依赖。X是超码。A是主属性,也就是属于某个候选码。
本题候选码 AC,主属性是 A, C。而 D, B, E, F 都是非主属性。
C -> D、A -> B、B -> F 的左边都不是超码,右边也不是主属性,所以也不满足 3NF。
第四部分:最小依赖集
最小依赖集也叫最小覆盖。这类题经常让你“化简函数依赖集”。
目标:把函数依赖集变成没有废话的等价版本。
步骤:
- 右边单属性化:把
X -> AB拆成X -> A和X -> B。 - 去掉左边冗余属性:如果
AB -> C中去掉A后,B -> C仍能由依赖集推出,那么A冗余。 - 去掉冗余依赖:如果删除某条依赖后,它仍能被其他依赖推出,那么这条依赖冗余。
例子:
1 | F = { A -> BC, B -> C, A -> B, AB -> C } |
第一步拆右边:
1 | A -> B |
其中 A -> C 可以由 A -> B 和 B -> C 推出,所以冗余。
AB -> C 左边的 A 或 B 也可能冗余,需要用闭包检验。
最后可能得到:
1 | Fmin = { A -> B, B -> C } |
写题时不要跳步,至少写出“拆右边、查左边、删冗余”三句话。
第五部分:模式分解
关系模式分解题通常问两个性质:
- 是否无损连接
- 是否保持函数依赖
1. 无损连接
把 R 分解成 R1 和 R2 后,如果自然连接回来不会多出假元组,就叫无损连接。
二元分解的快速判定:
若:
1 | R1 ∩ R2 -> R1 - R2 |
或:
1 | R1 ∩ R2 -> R2 - R1 |
则分解是无损连接。
大白话:两张表重叠的公共属性,必须能唯一控制其中一边的剩余属性,这样拼回去才不会乱配。
例子:
1 | R(A, B, C) |
公共属性是 A。
因为:
1 | A -> B |
也就是:
1 | R1 ∩ R2 -> R1 - R2 |
所以无损连接。
2. 保持函数依赖
如果原来的每条函数依赖,都能在分解后的各个小关系里分别检查,或者由小关系上的依赖联合推出,就叫保持函数依赖。
它关注的是:约束能不能局部检查。
如果不保持依赖,就可能出现一种尴尬情况:你必须把多张表连接回来,才能判断某条约束有没有被破坏。
3. BCNF 分解怎么拆
如果 R 中存在违反 BCNF 的依赖:
1 | X -> Y |
其中 X 不是超码,那么可以分解为:
1 | R1 = X ∪ Y |
意思是:先把“能被 X 直接管住的一小团属性”单独拿出来;原表里剩下的部分也不能把公共钥匙 X 丢掉,否则拆开之后就拼不回去了。
继续检查新关系,直到每个关系都满足 BCNF。
注意:BCNF 分解通常能保证无损连接,但不一定保持函数依赖。
4. 3NF 分解怎么拆
3NF 分解通常基于最小依赖集:
- 先求最小依赖集。
- 对每条依赖
X -> A建一个关系XA。 - 如果没有任何一个关系包含候选码,需要额外加入一个包含候选码的关系。
- 删除被其他关系完全包含的冗余关系。
3NF 分解的优点:通常可以做到无损连接并保持依赖。
第六部分:本章落笔顺序
遇到“给定 R 和 F,求候选码、判断范式、分解”的题,不要直接凭眼缘判断。它更像修一张混乱的档案表:先找钥匙,再看哪些信息放错抽屉,最后决定怎么拆。
- 写出全集
U和函数依赖集F。 - 找出没有出现在右边的属性,它们必须进入候选码。
- 算闭包,得到候选码。
- 标出主属性和非主属性。
- 逐条判断每个函数依赖左部是否为超码。
- 判断 BCNF;若不满足,再按 3NF 条件判断。
- 若要求分解,先说明采用 BCNF 分解还是 3NF 分解。
- 检查无损连接和依赖保持。
这章最忌讳“看一眼就说”。闭包是你的验算纸,别省。范式题很多错误不是公式不会,而是第一步钥匙找错了,后面拆得越认真,错得越整齐。
第六章:事务管理、并发控制与恢复
如果说前面的规范化是在防止“表设计时就埋雷”,那事务这一章就是在防止“系统运行时突然翻车”。
数据库最怕的不是慢一点,而是账错了。
比如转账,A 扣了 100,B 却没加上;又比如两个窗口同时卖最后一张票,结果一张票卖给了两个人。事务管理、并发控制与恢复,就是数据库给自己修的三道保险门:操作要成组、并发别打架、崩溃能补账。
第一部分:事务与事务四性
1. 事务是什么
事务是数据库中一个不可再分的工作单位。
例如转账:
1 | A 账户 -100 |
这两步必须作为一个整体成功,或者作为一个整体失败。不能 A 扣了钱,B 没收到。
2. 事务四性
事务的四个特性可以这样记:
- 原子性:事务中的操作要么全做,要么全不做。它主要靠日志恢复和 UNDO 来保证。
- 一致性:事务执行前后,数据库必须从一个一致状态进入另一个一致状态。它靠完整性约束、事务逻辑和恢复机制共同保证。
- 隔离性:多个事务并发执行时,彼此不能随意干扰,效果应该像按某种顺序串行执行。它靠封锁、时间戳、MVCC 等并发控制机制保证。
- 持久性:事务一旦提交,它对数据库的修改就应该永久保存。它主要靠日志、REDO 和稳定存储保证。
可以把它想成一次银行柜台办业务:要么整笔业务办完,要么整笔业务撤销;不能办一半;旁边柜台不能把你的账本抢过去乱写;盖章完成后,记录不能突然消失。
如果问为什么数据库系统要保证事务一致性,核心原因是:数据库不仅要存数据,还要维护约束、外键、业务规则。如果事务执行到一半被打断,数据库会落入不合法状态,后续所有查询都会基于错误数据继续扩散。
第二部分:并发调度
1. 调度是什么
多个事务并发执行时,它们的读写操作会交错排列,这个排列就叫调度。
例如:
1 | S: R1(A) W1(A) R2(A) W2(A) C1 C2 |
表示事务 T1 和 T2 的操作交错执行。
2. 串行调度
如果一个事务完全执行完,再执行另一个事务,就是串行调度。
例如:
1 | T1: R(A) W(A) COMMIT |
串行调度最安全,但效率低。
3. 可串行化调度
如果一个并发调度的执行结果等价于某个串行调度,就叫可串行化。
最常见的判断方法是:冲突可串行化。
第三部分:冲突可串行化
1. 什么操作会冲突
两个操作冲突,必须同时满足:
- 来自不同事务。
- 访问同一个数据项。
- 至少有一个是写操作。
冲突类型:
1 | R1(A) W2(A) |
不冲突:
1 | R1(A) R2(A) |
2. 优先图判定法
判断步骤:
- 为每个事务画一个节点。
- 从左到右扫描调度。
- 如果
Ti的操作先于Tj的冲突操作,就画边Ti -> Tj。 - 如果图中无环,则冲突可串行化。
- 如果有环,则不可冲突串行化。
3. 推演例子
给出:
1 | S: R2(A) R1(A) R2(B) W2(B) R1(A) W3(B) |
解题不要直接猜。逐对找冲突:
R2(B)在W3(B)前,且不同事务同一数据项,画T2 -> T3。W2(B)在W3(B)前,画T2 -> T3。- 只读之间不冲突。
最后看图有没有环。
如果无环,还可以根据拓扑排序写出等价串行顺序。
第四部分:封锁与两段锁协议
并发控制最经典的方法是封锁。
1. 共享锁与排他锁
共享锁用于读。一个事务给数据项加共享锁后,其他事务也可以读这个数据项,但不能写。
排他锁用于写。一个事务给数据项加排他锁后,其他事务既不能读,也不能写。
锁的兼容性记一句话就够了:读读可以共存,读写和写写都不行。 所以共享锁和共享锁兼容;共享锁和排他锁不兼容;排他锁和任何锁都不兼容。
2. 两段锁协议 2PL
两段锁协议把事务分成两个阶段:
- 增长阶段:只能加锁,不能释放锁。
- 收缩阶段:只能释放锁,不能再加锁。
满足 2PL 的调度一定是冲突可串行化的。
但注意:2PL 可能造成死锁。
3. 死锁
死锁就是事务互相等待对方释放资源。
例如:
1 | T1 锁住 A,等待 B |
处理方式:
- 预防:规定加锁顺序。
- 检测:画等待图,发现环。
- 解除:回滚某个事务。
第五部分:日志恢复
日志题的本质是补账:系统崩溃以后,数据库要根据账本判断哪些账已经盖章,哪些账只是写到一半。
1. 为什么需要日志
数据库不能只改数据页,因为崩溃可能发生在任何一刻。
日志记录了“我做过什么”,用于崩溃后恢复。
基本日志记录:
1 | <T1, START> |
含义:
START:事务开始。- 更新记录:事务修改了哪个数据项,旧值是什么,新值是什么。
COMMIT:事务提交。
2. 先写日志原则
所谓先写日志,就是先把账本写下来,再真正改数据库。
两条核心规则:
- 修改数据页之前,相关日志必须先落盘。
- 事务提交之前,提交日志必须先落盘。
没有这条规则,崩溃恢复就没有依据。因为你连“刚才到底想改什么”都没记住。
3. UNDO 与 REDO
UNDO 用来撤销未提交事务。事务还没来得及提交,系统就崩溃了,那么恢复时必须用日志里的旧值把它做过的修改撤回去。
REDO 用来重做已提交事务。事务已经提交,但对应的数据页可能还没真正写回磁盘,那么恢复时必须用日志里的新值再做一遍。
考试判断:
- 有
START但没有COMMIT:UNDO。 - 有
COMMIT:REDO。
4. 检查点
检查点的作用是缩小恢复范围。
常见日志:
1 | <T1, START> |
恢复时:
- 从最近的 checkpoint 开始看。
- checkpoint 时仍活跃的事务要继续追踪。
- 崩溃前已提交的事务进入 REDO 集。
- 崩溃前未提交的事务进入 UNDO 集。
5. 日志题怎么落笔
看到日志题,先画两列:
1 | UNDO 集: |
然后从日志尾部往前看:
- 碰到
COMMIT,该事务放入 REDO。 - 碰到
START但没见COMMIT,该事务放入 UNDO。 - 碰到
CHECKPOINT,把检查点中的活跃事务纳入判断范围。 - 对 UNDO 事务按日志反向撤销。
- 对 REDO 事务按日志正向重做。
考试问最终数据值时,就按这个顺序更新:
- REDO 用新值。
- UNDO 恢复旧值。
- 没被相关日志影响的数据保持原值。
第六部分:本章自测
题 1: 为什么转账操作必须放进一个事务?
答题关键词:原子性、一致性、崩溃恢复、不能只扣不加。
题 2: 判断调度是否冲突可串行化,为什么不能只看事务编号顺序?
答题关键词:冲突操作、同一数据项、至少一个写、优先图、有环。
题 3: 日志中某事务有 START 但没有 COMMIT,崩溃恢复时应该 UNDO 还是 REDO?
答案:UNDO。因为它没有提交,不能让未完成事务的结果留在数据库里。
第七章:SQL 进阶应用
第三章讲的是 SQL 的基本句型,这一章更像“把句子写成文章”。
现实里的数据库操作很少只有一句简单查询。它经常是:先把几张表拼起来,再筛选,再分组;有些查询太常用,就封成视图;有些规则必须自动检查,就交给触发器;有些流程反复执行,就写成存储过程。
所以这一章不是另起炉灶,而是把 SQL 从“单句口令”升级成“可复用的工具箱”。
第一部分:复杂 SQL 的翻译手感
1. 多表查询
需求:查询借阅过某本书的读者姓名、书名、借阅日期。
先别急着写代码,先像拆快递一样拆需求:
- 找输出列在哪些表。
- 找表之间靠什么外键连接。
- 把筛选条件写进
WHERE或ON。
对应 SQL 可以写成:
1 | SELECT r.readerName, b.bookName, bo.borrowDate |
2. 分组统计
需求:统计每个班的平均分,只显示平均分大于 60 的班级。
1 | SELECT classNo, AVG(score) AS avgScore |
记住:
WHERE筛选分组前的行。HAVING筛选分组后的组。
3. “没有……”题型
需求:查询没有借过书的读者。
写法一:NOT EXISTS
1 | SELECT r.readerNo, r.readerName |
写法二:左连接空值判断
1 | SELECT r.readerNo, r.readerName |
遇到“没有选课”“没有成绩”“没有借阅记录”,优先想这两个写法。
4. “全部……”题型
需求:查询选修了全部课程的学生。
经典双重 NOT EXISTS:
1 | SELECT s.sno, s.sname |
逻辑翻译:
1 | 不存在这样一门课:这个学生没有选。 |
“全部”在 SQL 里经常要翻译成“没有一个不满足”。
第二部分:视图
视图是从基本表或其他视图导出的虚表。
它本身通常不直接存数据,而是保存查询定义。
你可以把视图理解成一扇固定角度的窗。底下的表是整栋楼,视图只让某类用户从某个角度看见其中一部分。窗外的真实建筑变了,只要窗口设计得好,用户仍然像以前一样看数据。
1. 创建视图
1 | CREATE VIEW ViewName AS |
例子:创建一个外单位读者借阅情况视图。
1 | CREATE VIEW V_ExternalBorrow AS |
2. 使用视图
1 | SELECT * |
3. 视图的作用
- 简化复杂查询。
- 屏蔽底层表结构。
- 控制用户能看到的数据范围。
- 提供一定程度的逻辑数据独立性。
考试问视图优点时,别只写“方便”,要写“简化查询、安全控制、逻辑独立性”。
第三部分:触发器
触发器是当某个表发生 INSERT、UPDATE、DELETE 时自动执行的程序。
它像门口的自动安检机。你不是每次都靠人工提醒“别带违禁品”,而是只要有人进门,机器自动扫一遍,发现不合法就拦下。
1. 触发器基本结构
不同数据库语法略有差异,考试通常看你是否理解逻辑。
通用模板:
1 | CREATE TRIGGER trigger_name |
2. 例子:限制借阅日期不能晚于还书日期
1 | CREATE TRIGGER trg_check_borrow_date |
如果是 SQL Server 风格,可能写成 CREATE TRIGGER ... AS BEGIN ... END;如果是 MySQL 风格,会有 DELIMITER。考试重点不是方言,而是:触发时机、触发表、触发事件、检查逻辑。
3. 触发器适合做什么
- 自动检查复杂完整性约束。
- 自动维护统计字段。
- 自动写审计日志。
- 自动阻止不合法更新。
但触发器不能乱用。它隐藏在表操作背后,太多会让系统难调试。
第四部分:存储过程
存储过程是一段保存在数据库里的程序,可以带输入参数和输出参数。
1. 基本思想
普通 SQL 像一句命令,存储过程像一个函数。
如果某套操作你每天都要做,比如查图书信息、计算借阅天数、批量更新状态,那就不要每次从头敲一遍。把它封装成存储过程,就像把一串常用动作录成宏:输入参数,数据库自己按步骤做完。
它适合:
- 封装复杂查询。
- 重复执行固定业务逻辑。
- 给应用层提供统一接口。
2. MySQL 风格模板
1 | DELIMITER // |
调用:
1 | CALL procQueryBook('B001'); |
3. 带输出参数的写法
1 | DELIMITER // |
考试中如果题目提到 DATEDIFF,通常是在让你计算借阅天数、间隔天数、逾期天数。
第五部分:建表题完整写法
试卷会给你若干表结构,让你写 CREATE TABLE。不要漏主键、外键、非空、检查约束。
例子:成绩表。
1 | CREATE TABLE Score ( |
写题检查清单:
- 每个字段有没有类型。
- 主键是否完整,复合主键必须写表级约束。
- 外键是否指向正确的父表。
- 业务范围能否用
CHECK表达。 - 是否需要
NOT NULL。
第六部分:关系代数表达式怎么写
关系代数题通常要求你写:
1 | 查询出版社为“第三书屋”的图书类别名、书名、图书编号 |
写法不要一上来就连接全部表。按“选择提前,投影收尾”:
1 | π_className, bookName, bookNo( |
更严谨一点可以写连接条件:
1 | π_className, bookName, bookNo( |
考试给分点:
- 是否连接了必要的表。
- 是否写对选择条件。
- 是否最后投影目标列。
- 是否没有引入无关表。
第七部分:SQL 大题的关键词反射
写完 SQL 后要按关键词自查。
看到“所有、全部”,优先想到双重 NOT EXISTS 或关系代数里的除法思想。
看到“没有、未、从未”,优先想到 NOT EXISTS,或者 LEFT JOIN ... IS NULL。
看到“每个、各个”,通常需要 GROUP BY。
看到“平均、总数、最大、最小”,通常需要聚合函数。
看到“平均值大于某条件”,通常是分组后的筛选,要写 HAVING,不是 WHERE。
看到“即使没有也要显示”,通常需要外连接。
看到“创建派生查询对象”,通常是 CREATE VIEW。
看到“插入或更新时自动检查”,通常是 CREATE TRIGGER。
看到“封装查询过程”,通常是 CREATE PROCEDURE。
看到“日期相差”,通常用 DATEDIFF。
第八章:数据库设计综合练习
数据库设计综合题看起来像作文题,其实是在考你会不会把一段现实业务翻译成表。
这件事特别像整理一个社团的档案柜:哪些东西应该单独建档案袋,哪些只是档案袋上的标签,哪些是两份档案之间发生的一次事件。只要这三类分清楚,后面的主键、外键、联系表就会自然长出来。
第一部分:先把现实世界拆成三类东西
动笔之前先走五步:
- 读需求,圈名词:名词通常是实体候选。
- 圈动词,找联系:动词或业务事件通常是联系候选。
- 判断联系基数:1:1、1:n、m:n。
- 找联系属性:如果某个属性描述的是“一次关系”,就放到联系表里。
- 转关系模式:列出表、主键、外键。
不要一上来就画图。先拆语义。图只是结果,真正关键的是你有没有分清“对象、描述、事件”。
第二部分:实体、属性、联系的判断
1. 什么适合做实体
稳定、可独立区分、系统需要长期管理的对象,适合做实体。
例如:
- 学生
- 教师
- 图书
- 读者
- 课程
- 班级
- 快递员
- 客户
2. 什么适合做属性
依附于实体的描述信息,适合做属性。它本身通常不独立生活,而是贴在某个对象身上的标签。
例如:
- 学生姓名
- 图书价格
- 读者电话
- 课程学分
3. 什么适合做联系
实体之间发生的业务事件,适合做联系。
例如:
- 学生选修课程。
- 读者借阅图书。
- 快递员配送订单。
- 客户订购商品。
如果联系本身有属性,就要高度重视。因为这说明它不是一根普通连线,而是一张真正需要登记的“事件单”。
例如:
1 | 读者 借阅 图书 |
这个联系可能有:
- 借阅日期
- 归还日期
- 是否续借
这些属性不属于读者,也不属于图书,而属于“这一次借阅”。读者没有固定的“借阅日期”,图书也没有固定的“归还日期”,只有读者借某本书的那一次事件才有这些信息。
所以逻辑设计时需要:
1 | Borrow(readerNo, bookNo, borrowDate, returnDate, ...) |
第三部分:从 E-R 到关系模式
1. 实体转表
实体集直接转关系表:
1 | Reader(readerNo, readerName, workUnit, phone) |
2. 1:n 联系
把 1 端主键放到 n 端作为外键。
例如:
1 | 一个班级有多个学生,一个学生属于一个班级 |
转成:
1 | Class(classNo, className) |
Student.classNo 是外键。
3. m:n 联系
必须单独建联系表。
例如:
1 | 学生选修课程 |
转成:
1 | Student(studentNo, studentName) |
Score 的主键通常是 (studentNo, courseNo)。
如果同一学生同一课程可多学期重复修读,就要把 termNo 也放进主键:
1 | Score(studentNo, courseNo, termNo, score) |
这就是业务规则改变主键的典型例子。
4. 弱实体
弱实体的主键通常包含强实体主键。
例如:
1 | Order(orderNo, orderDate) |
OrderItem 离开 Order 没有意义,所以它的主键常见为 (orderNo, itemNo)。
第四部分:试卷型综合设计写法
假设题目给出“图书借阅系统”:
需求:
- 每本图书属于一个类别。
- 每本图书由一个出版社出版。
- 读者可以多次借阅图书。
- 每次借阅要记录借阅日期、归还日期。
1. 实体集
1 | Book(bookNo, bookName, author, price) |
2. 联系集
1 | Book - BookClass:n:1 |
3. 关系模式
1 | BookClass(classNo, className) |
4. 主键与外键
1 | BookClass 主键:classNo |
为什么 Borrow 的主键要加 borrowDate?
因为同一个读者可能在不同时间多次借同一本书。如果主键只写 (readerNo, bookNo),第二次借同一本书时就会冲突。
这和前面“考试成绩表要不要加 ExamDate”的逻辑完全一样。
第五部分:ScoreDB 选课系统例子
试卷常给:
1 | Student(studentNo, studentName, sex, birthday, native, nation, classNo) |
你要立刻看出:
Student.classNo指向Class.classNo。Score.studentNo指向Student.studentNo。Score.courseNo指向Course.courseNo。Score.termNo指向Term.termNo。Score是学生、课程、学期之间的联系记录。
Score 的主键通常是:
1 | (studentNo, courseNo, termNo) |
因为同一个学生同一门课在不同学期可能有不同成绩。
常见查询:
查询某班所有学生成绩:
1 | SELECT s.studentNo, s.studentName, c.courseName, t.termName, sc.score |
查询没有成绩的学生:
1 | SELECT s.studentNo, s.studentName |
第六部分:综合设计题得分点
阅卷时通常按点给分,你要让老师一眼看到这些东西:
- 实体集是否完整。
- 属性是否放在正确实体或联系上。
- 联系类型是否标清:1:1、1:n、m:n。
- m:n 联系是否转成独立关系表。
- 联系属性是否进入联系表。
- 主键是否能唯一标识元组。
- 外键是否写清楚引用关系。
- 是否避免多值属性、重复组、冗余字段。
考试里最稳的书写格式:
1 | 实体: |
这样写,哪怕图画得一般,文字分也能拿得很稳。
整篇笔记的复习顺序
如果时间紧,就按这个顺序复习:
- 第一章:三级模式、两级映射、数据独立性。
- 第二章:关系模型术语、三类完整性、关系代数表达式。
- 第三章:SQL 查询、连接、分组、子查询、建表约束。
- 第四章:E-R 图、联系属性、E-R 转关系表。
- 第五章:闭包、候选码、3NF、BCNF、无损连接、依赖保持。
- 第六章:ACID、冲突可串行化、两段锁、日志恢复。
- 第七章:视图、触发器、存储过程、复杂 SQL 翻译。
- 第八章:综合设计题写法。
最后提醒一句:数据库考试的高分不是靠背概念堆出来的,而是靠“流程化答题”。遇到大题时,按模板一步步写,别急着跳到答案。