Skip to content

数据库与中间件(MySQL、Redis)· 选择题缺口补题(30 道 · 含答案解析) ​

定位: 《数据库与中间件(MySQL、Redis)高频选择题60道(含答案解析)》的选择题补充,用于补齐主库在两处已知缺口:① 数据库原理层完全空缺(主库 60 题中范式、关系代数、关系模型、ER 图、三级模式、检查点、两段锁等关键词命中数为 0);② 面试向高频考点遗漏(Redis 限流、编码细节、集群迁移;MySQL 乐观/悲观锁、Buffer Pool、死锁检测、索引下推、表设计)。 适用场景: ① 国企央企笔试「数据库系统」科目(国家电网计算机类考纲 6 条:数据库基本概念 / 关系数据库基本理论 / 关系数据库标准语言 SQL / 事务处理和并发控制 / 备份和恢复 / 数据库应用系统设计与开发)② 互联网笔试选择题(牛客 2026 官方知识点表:「计算机基础 → 数据库」= 关系的键与完整性、关系模型结构和定义;旁证指南列 索引 / 事务 / 关系模型)③ 互联网面试八股(MySQL 索引链、Redis 中间件)。 ⚠️ 范围说明: 本文件全部为笔试选择题(单选,四选一),共 30 道。国企央企笔试实际使用单选 + 多选 + 判断三种题型(国网约 165 题中多选 25 题、判断 30 题),本文件只覆盖单选这一种;多选与判断题不在本文件范围内,需另行补题。互联网大厂校招机考为纯编程题,本文件不承担该职能。 高频依据: ① 国家电网《高校毕业生招聘考试大纲(计算机类专业 2026 版)》原文(数据库系统 6 条知识点,全为教材原理,无 MySQL/Redis 产品条目);② 牛客网《2026 大厂校招笔试指南》官方三级知识点表(计算机基础 → 数据库;全表无 Redis/中间件);③ 八股精 MySQL / Redis 面试高频词频统计(MySQL:索引 11.18% > SQL 6.20% > B+树 4.12% > 事务 3.55% > 隔离级别 2.73% > MVCC 1.97%;Redis:数据结构 5.73% > 分布式锁 4.94% > 穿透/击穿/雪崩 3.71%/3.59%/3.06%)。 与主库的关系: 主库 60 题 100% 为 MySQL / Redis 产品机制(索引、事务锁、日志复制、缓存三问、持久化、集群);本补题 30 道以 数据库原理教材体系 为主干(补-01 ~ 补-20),辅以主库未覆盖的 MySQL 深度考点 5 题(补-21 ~ 补-25)与 Redis 深度考点 5 题(补-26 ~ 补-30)。两者互补、不重复。 核验依据: 答案键与解析已按本文件正文逐题核对;历史考点核验报告未随本库分发。


一、数据库基本概念与关系模型(补-01 ~ 补-05) ​

补-01 ​

数据库系统的三级模式结构中,「外模式 / 模式映射」与「模式 / 内模式映射」分别保证的是( )。 A. 物理独立性;逻辑独立性 B. 逻辑独立性;逻辑独立性 C. 逻辑独立性;物理独立性 D. 两者都与数据独立性无关

答案:C

📌 考点定位: 数据库基本概念——三级模式与两级映射,对应国网考纲第 10 条,国企笔试常考。

【结论】 选 C。外模式 / 模式映射保证逻辑独立性,模式 / 内模式映射保证物理独立性;其本质是「哪一层发生了变化,就让映射去吸收这个变化,使相邻的另一层不受影响」。

【逐项辨析】

  • A 物理独立性;逻辑独立性:错。把两级映射的职责整体说反了——外层映射(外模式 / 模式)管的是逻辑独立性,内层映射(模式 / 内模式)管的才是物理独立性。
  • B 逻辑独立性;逻辑独立性:错。把「模式 / 内模式映射」也写成逻辑独立性,等于抹掉了物理独立性这一层,两级映射失去分工。
  • C 逻辑独立性;物理独立性:正确。 外模式 / 模式映射位于外模式与模式之间,模式改变时调整它即可保持外模式不变,故保证逻辑独立性;模式 / 内模式映射位于模式与内模式之间,内模式改变时调整它即可保持模式不变,故保证物理独立性。
  • D 两者都与数据独立性无关:错。两级映射恰恰是数据独立性的实现机制,说「无关」是错的。

【知识点】 三级模式是对数据的三个抽象级别,两级映射是相邻级别之间的转换规则,二者共同构成数据独立性的保障机制。

层次名称描述对象面向对象数量
外层外模式(子模式 / 用户模式)用户能看到的局部数据视图用户 / 应用程序多个
中层模式(逻辑模式)全体数据的全局逻辑结构数据库设计人员一个
内层内模式(存储模式)数据的物理存储结构DBMS一个

两级映射与两类独立性的推导逻辑是「变化 → 调整映射 → 上层保持不变」:

text
模式改变   → 改外模式 / 模式映射 → 外模式不变 → 应用程序不变 → 逻辑独立性
内模式改变 → 改模式 / 内模式映射 → 模式不变   → 外模式不变   → 物理独立性

判断的关键在于独立性保护的是谁:逻辑独立性保护的是「外模式(进而应用程序)不受模式变化影响」,物理独立性保护的是「模式不受内模式变化影响」。因此可以概括为「外层映射管逻辑、内层映射管物理」。

补充两点:外模式 / 模式映射可以有多个(多个外模式对应同一个模式),模式 / 内模式映射只有一个(一个模式对应一个内模式);两级映射均由 DBMS 负责维护,用户程序不感知。

【记忆锚点】 「外逻辑、内物理」——越靠外的映射管逻辑独立性,越靠内的映射管物理独立性。

【易混对比】

  • 换个问法:「当数据库的存储结构改变时,DBA 只需修改模式 / 内模式映射,从而保证数据的( )不变」→ 答物理独立性。注意题目问的是「保证谁不变」,与「哪个映射保证哪种独立性」是同一考点的两种问法。
  • 与三级模式的定义连考:问「一个数据库有几个外模式 / 模式 / 内模式」→ 外模式多个、模式一个、内模式一个。
  • 易混概念:逻辑独立性 ↔ 物理独立性。前者对应模式变化,后者对应内模式变化,两者不可互换。

【自测】

某数据库把数据文件的存储组织方式由顺序组织改为散列组织,DBA 只需修改模式 / 内模式映射,无需改动模式与外模式。这体现了( )。 A. 逻辑独立性 B. 物理独立性 C. 参照完整性 D. 实体完整性 答:B。改变的是内模式(物理存储结构),被保护不变的是模式,属物理独立性。(关联补-01 同一考点的反向问法)

【选项误解】

  • A 误解来源:把「外→内」的顺序误记成「物理→逻辑」。常见心理是「越靠外越物理(用户直接摸数据)」,实际恰恰相反——外层映射保护的是逻辑视图。
  • B 误解来源:认为「两级映射都是为了逻辑独立」,把物理独立性这一层抹掉。多出现在只背「数据独立性」四个字、未区分「逻辑/物理」的考生。
  • C 正确项:外模式/模式映射管逻辑,模式/内模式映射管物理。
  • D 误解来源:把「映射」当成可有可无的实现细节。实际上两级映射是数据独立性的实现机制,教材原话即「通过两级映像功能保证数据的逻辑独立性与物理独立性」。

【知识关联】

  • 主库关联:主库 60 题几乎全是 MySQL/Redis 产品机制,三级模式属教材原理层空缺,主库无直接对应题;可对照补-10(视图=外模式的实现载体)。
  • 国网/408:国网考纲第 10 条「数据库基本概念」直接命中;408 数据库部分常考「三级模式/两级映射/数据独立性」填空与选择,属送分题。
  • 面试追问:① 逻辑独立性在生产中如何体现?→ 基表加列/改列时,通过重定义视图(外模式)让旧应用 SQL 不改仍能跑。② 物理独立性如何体现?→ 换存储引擎、改行格式、调整页大小时只改内模式映射,SQL 层无感。③ 视图算外模式吗?→ 视图是外模式的常见实现手段之一,外模式还可包括用户级权限可见范围。

【拓展延伸】

  • 变式问法:①「模式改变时只需修改外模式/模式映射,保证了数据的(逻辑独立性)」;②「内模式改变时模式不变,体现的是(物理独立性)」;③「一个数据库中模式/内模式各有几个?」→ 各一个,外模式可有多个。
  • 工程参数/命令:MySQL 中 CREATE VIEW / ALTER VIEW 即在维护「外模式—模式」映射;ALTER TABLE ... ENGINE=InnoDB、调整 innodb_page_size(需重建实例)属内模式变化;information_schema.TABLES / SHOW CREATE VIEW 可查看视图定义(外模式)。Oracle 的 synonym、DB link 也承担类似外模式角色。
  • 工程直觉:ORM 框架 + 数据库视图常被联合用来做「逻辑独立性」的工程化落地——表结构演进时优先改视图,少改业务代码。

补-02 ​

在一个关系模式中,能唯一标识元组、且不含任何多余属性的属性组,称为( )。 A. 候选键(候选码) B. 超键 C. 主键 D. 外键

答案:A

📌 考点定位: 关系数据库基本理论——键的概念,对应国网考纲第 11 条;也是牛客笔试选择题「关系的键与完整性」的核心。

【结论】 选 A。能唯一标识元组且不含任何多余属性的属性组是候选键,其判定标准是「极小性」,题干中的「不含任何多余属性」正是极小性的表述。

【逐项辨析】

  • A 候选键(候选码):正确。 候选键 = 超键 + 极小性,即不含多余属性的超键,与题干「能唯一标识元组、且不含任何多余属性」逐字对应。
  • B 超键:错。超键只要求「能唯一标识元组」,不要求极小性,允许含多余属性,与题干后半句「不含任何多余属性」冲突。
  • C 主键:错。主键是从多个候选键中人为选定的那一个,题干描述的是「所有满足极小性的键」,未涉及选定行为,用主键来定义不够严谨。
  • D 外键:错。外键是本关系中引用另一关系主键(或候选键)的属性组,作用是表达关系之间的联系,并不唯一标识本关系的元组。

【知识点】 键的四个概念是层层递进、逐级加约束的关系:

概念定义唯一标识元组要求极小性人为指定
超键能唯一标识元组的属性组是否否
候选键不含多余属性的超键(极小超键)是是否
主键从候选键中选定一个是是是
外键引用另一关系主键 / 候选键的属性组否—否

包含关系可写成:候选键 ⊆ 超键,且主键 ∈ 候选键。

候选键的求解方法(属性闭包法):

  1. 把只在函数依赖左边出现的属性记为 L 类,只在右边出现的记为 R 类,两边都出现的记为 LR 类,都不出现的记为 N 类。
  2. 必然属于候选键的属性是 L 类与 N 类。
  3. 对 L + N 求属性闭包,若闭包已包含全部属性,则该组合即为候选键;否则依次加入 LR 类属性再求闭包,能推出全部属性的极小组合即为候选键。

注意:候选键可以由单个属性构成,也可以由多个属性联合构成(复合候选键);一个关系可以有多个候选键,但只能有一个主键。

【完整推导·属性闭包法】 例:R(A,B,C,D),FD = {A→B, B→C, A→D}。

  1. 出现位置:A 在左;B 左右都有(LR);C 只在右(R);D 只在右(R)。
  2. L={A},N=∅,R={C,D},LR={B}。先试 L:A⁺。
  3. A⁺:由 A→B 得 {A,B};由 B→C 得 {A,B,C};由 A→D 得 {A,B,C,D} = 全部属性。
  4. A 已是候选键(单属性、极小);任何真子集 ∅ 推不出 A,满足极小性。
  5. AB⁺、AC⁺、ABCD⁺ 虽也能唯一标识,但含多余属性 → 只是超键,不是候选键。 结论:候选键 = {A},若设计表则主键可取 A。

【记忆锚点】 「超键不极小,候选键极小,主键是候选键里挑出来的那一个」。

【易混对比】

  • 换个问法:「能唯一标识元组但可以含多余属性的属性组」→ 答超键;「能唯一标识元组且一个属性都不能少」→ 答候选键。两者的分界线就是「极小性」三个字。
  • 与实体完整性连考:主键的取值约束是「非空 + 唯一」;候选键只要求唯一,在 SQL 中通常以 UNIQUE 约束实现,允许多个 NULL,这与主键的 PRIMARY KEY 不同。
  • 易混概念:外键不属于键的「强弱」等级。候选键、超键、主键都是「标识本关系元组」,而外键是「引用其他关系」,二者不在同一分类维度上。

【自测】

关系模式 R(A, B, C, D) 的函数依赖集为 {A → B, B → C, A → D},则 R 的候选键是( )。 A. A B. AB C. AC D. ABCD 答:A。A 的闭包为 {A, B, C, D},已包含全部属性且为单属性,故 A 是候选键;B 推不出 A 与 D,AB、AC、ABCD 都含多余属性,属超键而非候选键。(关联补-02 候选键的极小性)

【选项误解】

  • A 正确项:候选键 = 超键 + 极小性,与「不含任何多余属性」逐字对应。
  • B 误解来源:只记住「能唯一标识元组」,漏掉后半句「不含多余属性」。超键允许含多余属性(如 ABCD 在单键 A 已够时仍是超键)。
  • C 误解来源:把「候选」与「主」混为一谈。主键是人为从候选键中选定的一个,题干描述的是客观满足极小性的那一类键,不是选定结果。
  • D 误解来源:把「键」字一概理解为主键/外键。外键解决的是关系之间的引用,不唯一标识本关系元组,与题干无关。

【知识关联】

  • 主库关联:M01(聚簇索引叶子=整行,主键即聚簇键)、M22(为何建议自增主键)、M23(UUID 作主键的缺点)。候选键/主键是这些题的理论前置。
  • 国网/408:国网考纲第 11 条「关系数据库基本理论」;408 常考「求候选键」计算题(属性闭包法)。
  • 面试追问:① 一个表能否没有主键?→ 逻辑上可以,但 InnoDB 会隐式生成 6 字节 ROWID 作聚簇索引键。② 候选键与唯一索引什么关系?→ 候选键在 SQL 中常以多个 UNIQUE NOT NULL 实现,但 UNIQUE 允许多 NULL 时不完全等价。③ 主键为何只能一个、候选键可多个?→ 主键是设计决策(选定一个),候选键是语义推导结果。

【拓展延伸】

  • 变式问法:①「不含多余属性的超键」→ 候选键;②「从候选键中选定的那个」→ 主键;③「能唯一标识但可含多余属性」→ 超键;④「R(A,B,C,D),FD={AB→C, C→D, A→B},求候选键」→ 闭包法实算:只在左边出现的是 A(B/C/D 均出现在右侧),A⁺:A→(A→B)AB→(AB→C)ABC→(C→D)ABCD = 全属性,故候选键唯一为 {A};AB 是超键但含多余属性,不是候选键。
  • 工程参数/命令:SHOW KEYS FROM t WHERE Key_name='PRIMARY';CREATE TABLE ... PRIMARY KEY (a,b);UNIQUE KEY 与主键差异:唯一键允许多 NULL、可有多个,主键非空唯一且仅一个。InnoDB 中主键长度会进入所有二级索引叶子,故主键宜短(自增 BIGINT 优于 UUID varchar(36))。
  • 属性闭包速算:L(只在左出现)必在候选键中;N(从不出现)必在候选键中;R(只在右出现)必不在;对 L+N 求闭包,不够再补 LR。

补-03 ​

下列约束中,属于「参照完整性」的是( )。 A. 主键的值不能为空且不能重复 B. 学生的年龄必须取值在 16~60 之间 C. 性别只能取「男」或「女」 D. 外键的取值必须是被参照关系中某个主键的值,或者取空值

答案:D

📌 考点定位: 关系数据库基本理论——三类完整性约束,对应国网考纲第 11 条,国企 / 银行笔试高频。

【结论】 选 D。参照完整性约束的是外键与被参照关系主键之间的取值对应关系,其标准表述正是「外键取值必须是被参照关系中某个主键的值,或者取空值」。

【逐项辨析】

  • A 主键的值不能为空且不能重复:错。这是实体完整性,约束对象是主键,与参照关系无关。
  • B 学生的年龄必须取值在 16~60 之间:错。这是针对具体业务语义的用户定义完整性(域约束),不涉及关系之间的引用。
  • C 性别只能取「男」或「女」:错。同样是用户定义完整性中的取值域约束,属业务规则。
  • D 外键的取值必须是被参照关系中某个主键的值,或者取空值:正确。 前半句保证「被引用的目标真实存在」,后半句允许「联系尚未确定」,二者合起来正是参照完整性的完整定义。

【知识点】 关系模型有三类完整性约束,作用对象与检查方式各不相同:

约束类型约束对象核心规则典型实现
实体完整性主键非空且唯一PRIMARY KEY
参照完整性外键取值等于被参照关系某主键值,或为 NULLFOREIGN KEY ... REFERENCES
用户定义完整性具体属性 / 业务规则取值范围、默认值、非空、CHECK 等CHECK、DEFAULT、NOT NULL

关于参照完整性,有三点必须掌握:

  1. 允许为空的前提:外键为 NULL 表示「该元组尚未建立联系」;若外键列同时被声明为 NOT NULL,则不允许取空值。
  2. 违约处理:当对被参照关系执行删除或更新导致外键失去引用目标时,DBMS 按声明的方式处理——RESTRICT / NO ACTION(拒绝)、CASCADE(级联删除或更新)、SET NULL(置空)。
  3. 检查时机:在默认的立即检查(NOT DEFERRABLE)模式下,约束在语句执行结束时校验,因此先插入被参照元组、再插入参照元组的顺序是安全的。

【记忆锚点】 「主键非空唯一 = 实体,外键有效 = 参照,业务规则 = 用户定义」。

【易混对比】

  • 换个问法:「CHECK(age BETWEEN 16 AND 60) 属于哪类完整性?」→ 答用户定义完整性,不是参照完整性。只要题干出现「具体数值范围、取值集合、默认值」,一律归用户定义完整性。
  • 与补-02 连考:候选键 / 主键属「键的概念」,实体完整性是「主键的取值规则」,两者常在同一道题里交叉出现。
  • 易混概念:参照完整性 ↔ 外键。参照完整性是规则,外键是实现该规则的载体;准确说法是「外键的取值约束属于参照完整性」。

【自测】

学生表 Student(Sno, Sname) 与选课表 SC(Sno, Cno, Grade) 中,SC.Sno 是外键。现要在 SC 中插入一条 Sno = '2025001' 的记录,而 Student 表中不存在该学号,DBMS 拒绝该插入,依据的是( )。 答:参照完整性。外键取值找不到对应的被参照主键值;若该外键列允许为空,也可改为插入 NULL 以表示联系待定。(关联补-03 三类完整性约束)

【选项误解】

  • A 误解来源:把「主键非空唯一」误当成参照完整性。主键规则约束的是本关系自身,属实体完整性;参照完整性约束的是外键与被参照表之间的取值对应。
  • B 误解来源:看到「必须」就往完整性上靠,但年龄范围是业务规则(用户定义完整性/域约束),不涉及关系引用。
  • C 误解来源:同 B,取值集合约束属用户定义完整性(CHECK)。
  • D 正确项:「等于被参照关系某主键值,或取空」是参照完整性的标准定义(Codd/教材原文口径)。

【知识关联】

  • 主库关联:M31(SQL 注入——参数校验属应用层完整性延伸);主库未覆盖外键/完整性,本题补原理缺口。
  • 国网/408:国网考纲第 11 条;408 数据库大题常考「指出违反哪类完整性」。
  • 面试追问:① 生产上还建议用数据库外键吗?→ 高并发互联网常逻辑外键(应用层校验 + 异步对账),因 FK 会带来锁竞争与级联风险;国企/银行核心库仍常用物理外键。② 外键为何常允许 NULL?→ 表示「联系尚未建立」,如订单尚未支付时 pay_id 可空。③ 删除被参照行时的四种策略?→ RESTRICT/NO ACTION(拒绝)、CASCADE(级联)、SET NULL(置空)。

【拓展延伸】

  • 变式问法:①「FOREIGN KEY (dept_id) REFERENCES dept(id) ON DELETE SET NULL 体现哪类完整性?」→ 参照完整性;②「CHECK (score BETWEEN 0 AND 100)」→ 用户定义完整性;③「插入 SC 时 Student 无此学号被拒」→ 参照完整性违约。
  • 工程参数/命令:
sql
ALTER TABLE sc ADD CONSTRAINT fk_sc_sno
  FOREIGN KEY (sno) REFERENCES student(sno)
  ON DELETE RESTRICT ON UPDATE CASCADE;
-- MySQL 查看外键
SELECT * FROM information_schema.TABLE_CONSTRAINTS
 WHERE CONSTRAINT_TYPE='FOREIGN KEY';
  • 级联风险:ON DELETE CASCADE 在父表误删时可瞬间清空子表,生产上更常用 RESTRICT + 应用层确认。

补-04 ​

关系代数中,「按条件筛选元组(行)」与「选取若干属性列」分别对应的运算符号是( )。 A. 投影 π;选择 σ B. 选择 σ;投影 π C. 连接 ⋈;除法 ÷ D. 并 ∪;差 −

答案:B

📌 考点定位: 关系数据库基本理论——关系代数运算,对应国网考纲第 11 条,国企笔试高频。

【结论】 选 B。选择 σ 按条件筛选行(元组),投影 π 选取指定列(属性);题干问的「筛选元组」对应 σ,「选取属性列」对应 π。

【逐项辨析】

  • A 投影 π;选择 σ:错。把两者的符号与含义整体颠倒——π 是投影(选列),σ 是选择(选行),题干后半句「选取若干属性列」应对应 π 而不是 σ。
  • B 选择 σ;投影 π:正确。 σ 后面跟条件表达式,作用于元组(行);π 后面跟属性名列表,作用于属性(列)。
  • C 连接 ⋈;除法 ÷:错。连接是按条件把两个关系组合成一个新关系,除法用于处理「对所有 / 全部」这类语义,二者都与题干描述的「单表筛选行与列」无关。
  • D 并 ∪;差 −:错。并、差是集合运算,要求参与运算的两个关系同构(属性个数与对应域相同),题干只涉及一个关系的行列筛选。

【知识点】 关系代数以关系为运算对象,按「能否由其他运算导出」分为基本运算与导出运算:

分类运算符号作用
基本运算并∪两个同构关系的元组合并
基本运算差−属于前者而不属于后者的元组
基本运算笛卡尔积×两关系元组的所有组合
基本运算选择σ按条件筛选行
基本运算投影π选取列并去除重复元组
导出运算交∩同属两个关系的元组
导出运算连接⋈按条件组合两个关系
导出运算除法÷处理「包含全部」语义

两个必须记住的细节:

  1. 投影会去重。π 的运算结果是一个集合,若选取列后出现完全相同的元组,会自动合并为一条。
  2. 选择的条件表达式可用 ∧(与)、∨(或)、¬(非)组合,例如 σ_age>20 ∧ sex='男'(R)。

写法示例:

text
σ_age>20(R)          从 R 中选出年龄大于 20 的元组(行)
π_name,age(R)        从 R 中取出 name、age 两列(列)

【记忆锚点】 「σ 是 Selection 选行、π 是 Projection 选列」——首字母 S 对 Selection、P 对 Projection,一一对应不会记混。

【易混对比】

  • 换个问法:「π_Sname(σ_Sdept='CS'(Student)) 的含义是什么?」→ 先按系别筛选行,再取姓名列,即「计算机系所有学生的姓名」。注意运算顺序由内向外。
  • 与补-05 连考:自然连接 ⋈ 属导出运算,其元组数受公共属性匹配情况影响,常与笛卡尔积 × 放在一起考。
  • 易混概念:选择 σ ↔ 投影 π(行 vs 列)、并 ∪ ↔ 交 ∩ ↔ 差 −(集合运算,要求同构)、连接 ⋈ ↔ 除法 ÷(组合关系 vs 全部语义)。

【自测】

关系代数表达式 π_Sname(σ_Sage>20(Student)) 的运算含义是( )。 A. 所有学生姓名 B. 年龄大于 20 的学生姓名 C. 所有学生年龄 D. 年龄大于 20 的学生学号 答:B。内层 σ 先按年龄筛选行,外层 π 再取姓名列。(关联补-04 选择与投影的组合)

【选项误解】

  • A 误解来源:σ/π 符号与含义整体对调。常见记忆错误是「π 像筛子(选行)」,实际 π 是 Projection(投影=选列)。
  • B 正确项:σ=Selection 选行,π=Projection 选列。
  • C 误解来源:把连接/除法当成基础筛选运算。它们处理的是多关系,题干只描述单表行列筛选。
  • D 误解来源:并/差是集合运算,要求同构关系,与「选行列」无关。

【知识关联】

  • 主库关联:M19(EXPLAIN type 访问路径——优化器把选择 σ 下推为索引 range/ref)、M20(Using filesort——投影/排序阶段代价)、M04(覆盖索引——投影所需的列都在索引里,免回表)。
  • 国网/408:国网考纲第 11 条;408 可能直接给关系代数表达式要求译义或写表达式。
  • 面试追问:① 为什么投影会去重?→ 关系是集合,集合元素唯一,π 结果自动去重。② SQL 的 WHERE 对应关系代数什么运算?→ 选择 σ。③ SELECT 列清单对应什么?→ 投影 π(但 SQL 结果是包/多重集,理论上去重不自动发生,除非 DISTINCT)。

【拓展延伸】

  • 变式问法:①「π_Sname(σ_Sdept='CS'(Student))」→ 计算机系学生姓名;②「列出选择运算的符号与含义」→ σ,按条件选行;③「哪些基本运算?」→ ∪、−、×、σ、π(五种)。
  • 工程参数/命令:优化器常把 WHERE 条件(σ)下推到存储引擎(ICP,见补-25);SELECT 列裁剪(π)影响是否走覆盖索引——SELECT * 会破坏覆盖索引,迫使回表。EXPLAIN 中 Using index = 投影所需列全在索引。
  • 推导顺序:表达式由内向外算——先 σ 筛行,再 π 取列;写 SQL 时逻辑执行顺序也是 WHERE(σ)先于 SELECT 列清单(π)生效。

补-05 ​

设关系 R(A, B) 有 3 个元组,关系 S(B, C) 有 4 个元组,则 R 与 S 的「自然连接」结果的元组个数( )。 A. 不超过 12 个,具体取决于公共属性 B 的取值匹配情况 B. 一定等于 12 个 C. 一定等于 3 个 D. 一定等于 7 个

答案:A

📌 考点定位: 关系数据库基本理论——自然连接与笛卡尔积的区别,国企 / 互联网笔试高频。

【结论】 选 A。自然连接先按公共属性做等值匹配再拼接,结果元组数取决于公共属性 B 的取值匹配情况,上界是 3 × 4 = 12、下界是 0,因此只能说「不超过 12 个」。

【逐项辨析】

  • A 不超过 12 个,具体取决于公共属性 B 的取值匹配情况:正确。 自然连接是「按公共属性等值匹配 + 去除重复列」的结果,能匹配出多少条取决于 B 的实际取值,故只能给出上界。
  • B 一定等于 12 个:错。这是笛卡尔积的结论;自然连接含等值筛选,B 值不匹配的元组会被丢弃,12 只是上界而非定值。
  • C 一定等于 3 个:错。3 是 R 的元组数,不是连接结果数;只有当 S 中每个元组至多匹配到 R 的一个元组时才会得到 3 个,题干并未给出这一前提。
  • D 一定等于 7 个:错。7 = 3 + 4 是「并」运算在同构前提下的元组数上限,与自然连接无关。

【知识点】 自然连接与笛卡尔积的区别集中在「是否做等值筛选」与「是否保留重复列」两点上:

对比项笛卡尔积 R × S自然连接 R ⋈ S
是否按公共属性筛选否,全组合是,只保留公共属性取值相等的组合
结果元组数恒为 |R| × |S| = 120 ≤ n ≤ |R| × |S| = 12
结果属性列R 与 S 的全部列,公共属性重复出现(A, B, B, C)公共属性只保留一次(A, B, C)
公共属性为空时—退化为笛卡尔积

自然连接元组数的推导过程:

text
设 R(A, B) 有 3 个元组,S(B, C) 有 4 个元组,公共属性为 B
step 1  取 R 的一个元组 r,在 S 中找 B 值相同的元组
step 2  r 与每个匹配元组拼接成一条新元组,属性列为 A, B, C
step 3  对 R 的 3 个元组重复 step 1 ~ 2,累加结果
上界:R 的 3 个元组都匹配上 S 的 4 个元组 → 3 × 4 = 12
下界:R 中所有 B 值在 S 中都不出现       → 0

三种典型取值情形(设 R、S 的 B 值都取自 {b₁, b₂, b₃},同侧各元组的 B 值可以重复):

  1. R 的 3 个元组 B 值全为 b₁,S 的 4 个元组 B 值也全为 b₁ → m(b₁)×n(b₁) = 3 × 4 = 12 个(取到上界的真实构造就是这种「两边取值完全对齐且可重复」);
  2. R 的 3 个元组 B 值互不相同(b₁、b₂、b₃),S 的 B 值只有 b₁ 且出现 4 次 → 结果为 1 × 4 = 4 个;
  3. S 的 B 值全部不在 R 的 B 值集合中 → 结果为 0 个。

补充:若连接条件不是「公共属性等值」而是任意条件,则称为 θ 连接(等值连接是 θ 取等号的特例),此时结果属性列不去重,元组数同样取决于匹配情况。

【完整推导·自然连接元组数】

text
形式化:R ⋈ S = π_{去重后属性}(σ_{R.B=S.B}(R × S))
步骤:
  1) 先做笛卡尔积:|R×S| = 3×4 = 12(上界)
  2) 再做选择 σ_{R.B=S.B}:只保留 B 值相等的组合
  3) 设 m(b) = |{r∈R | r.B=b}|,n(b) = |{s∈S | s.B=b}|
     则 |R⋈S| = Σ_b  m(b)×n(b)
  4) 由 Σ_b m(b)=3,Σ_b n(b)=4,且 m,n≥0
     ⇒ 0 ≤ Σ m(b)n(b) ≤ 3×4 = 12
     (柯西/排序不等式意义下,当两边取值完全对齐且可重复时取上界)
结论:自然连接元组数 ∈ [0, 12],题干未给 B 取值分布时只能选「不超过 12」。

对照表:

运算公式本题结果
笛卡尔积 R×S|R|×|S|恒 12
自然连接 R⋈SΣ_b m(b)·n(b)0~12
并 R∪S(若同构)≤ |R|+|S|≤7(本题不同构,不可并)

【记忆锚点】 「笛卡尔积是满配,自然连接是配对——配上了才有,配不上就没了」,即自然连接元组数 ≤ 笛卡尔积元组数。

【易混对比】

  • 换个问法:「若 R 有 3 个元组、S 有 4 个元组,则 R × S 的元组数与属性列数分别是多少?」→ 元组数 12,属性列数 = R 的列数 + S 的列数(公共属性重复计算)。
  • 再换问法:「若 R 的 B 值互不相同且都出现在 S 中,S 的每个 B 值恰好对应 1 个元组,则自然连接结果是多少个元组?」→ 3 个。可见「一定等于几」只有补充约束后才能确定。
  • 易混概念:自然连接 ⋈ ↔ 等值连接(前者去除重复列、后者保留重复列)、自然连接 ↔ 笛卡尔积(前者有筛选、后者无筛选)。

【自测】

关系 R(A, B) 有 5 个元组,S(B, C) 有 6 个元组,且 R 中 B 的取值互不相同、S 中 B 的取值也互不相同,两者 B 值有 2 个相同。则 R ⋈ S 的结果元组个数为( )。 A. 30 B. 2 C. 11 D. 无法确定 答:B。公共属性 B 上取值相同的只有 2 组,每组因两侧 B 值互不相同而只能拼出 1 个元组,故结果为 2 个;30 是笛卡尔积的结果。(关联补-05 自然连接的元组数)

【选项误解】

  • A 正确项:自然连接含等值筛选,上界 3×4=12,下界 0,具体取决于 B 的匹配情况。
  • B 误解来源:把自然连接当成笛卡尔积。笛卡尔积无筛选,元组数恒为 |R|×|S|。
  • C 误解来源:以为连接「以 R 为准」结果条数等于 R。只有当 S 中每个 B 值至多一条、且全部匹配时才是 3。
  • D 误解来源:3+4=7 是「并」运算在同构前提下的元组数上限,与连接无关。

【知识关联】

  • 主库关联:M04/M05(多表 JOIN 与索引使用——连接列建索引才能避免 NLJ 全表扫)、M19(EXPLAIN 中 join 类型)、M30(分库分表后跨库 JOIN 代价)。
  • 国网/408:国网第 11 条;408 数据库计算题高频——给 R、S 元组数与公共属性值域,求自然连接/笛卡尔积/外连接的元组数。
  • 面试追问:① 等值连接与自然连接区别?→ 自然连接去除重复公共属性列,等值连接保留。② 连接结果元组数何时最大?→ 公共属性值完全对齐且多对多时达到 |R|×|S|。③ MySQL 中 JOIN 默认是?→ INNER JOIN(内连接),不是自然连接。

【拓展延伸】

  • 变式问法:①「R×S 元组数?」→ 恒 12;②「R 有 3 元组、S 有 4 元组、公共属性 B 值全不同且 2 个相同」→ 自然连接 2 个;③「外连接元组数范围?」→ 左外 ≥|R|、右外 ≥|S|、全外 ≥max(|R|,|S|)(本例 R=3、S=4:左外最少 3 行,并非 max=4);上界取决于匹配,最坏 |R|×|S|。
  • 完整推导示例(补强):
text
R(B): {1,2,3}     S(B): {2,2,4,5}
自然连接:
  R 中 B=1 → S 中无 → 0
  R 中 B=2 → S 中 2 条 → 2
  R 中 B=3 → S 中无 → 0
  合计 2 条(属性 A,B,C)
笛卡尔积仍为 3×4=12
若 S(B)={1,2,3,3}:
  B=1→1 条,B=2→1 条,B=3→2 条,合计 4 条
  • 工程参数/命令:EXPLAIN 里 type=eq_ref 常表示被驱动表用唯一索引做等值连接;rows 列的乘积可粗估连接扇出。大表 JOIN 前务必确认连接列索引与驱动表选择。

补-06 ​

下列 SQL 语句中,属于 DCL(数据控制语言)的是( )。 A. CREATE、ALTER、DROP B. SELECT、INSERT、UPDATE、DELETE C. GRANT、REVOKE D. COMMIT、ROLLBACK、SAVEPOINT

答案:C

📌 考点定位: SQL 语言分类,对应国网考纲第 12 条,国企笔试常考。

【结论】 选 C。DCL 即数据控制语言,只管权限的授予与回收,GRANT(授权)与 REVOKE(收回权限)是它的全部成员。

【逐项辨析】

  • A CREATE、ALTER、DROP:错。这三条操作的是数据库对象的结构(库、表、索引、视图等),属 DDL(数据定义语言)。
  • B SELECT、INSERT、UPDATE、DELETE:错。这四条操作的是表中的数据,属 DML(数据操纵语言)。
  • C GRANT、REVOKE:正确。 二者负责把权限授予用户、从用户处回收权限,属 DCL(数据控制语言)。
  • D COMMIT、ROLLBACK、SAVEPOINT:错。这三条控制的是事务的提交、回滚与保存点,属 TCL(事务控制语言)。

【知识点】 SQL 按功能分为四类,分类依据是「操作对象是什么」:

类别全称操作对象代表语句
DDL数据定义语言数据库对象的结构CREATE、ALTER、DROP、TRUNCATE
DML数据操纵语言表中的数据SELECT、INSERT、UPDATE、DELETE
DCL数据控制语言访问权限GRANT、REVOKE
TCL事务控制语言事务边界COMMIT、ROLLBACK、SAVEPOINT

记忆要点:TRUNCATE 虽然效果是清空数据,但它属于 DDL(按对象结构操作、隐式提交、不可回滚),这一点最容易被误判成 DML。

【知识点扩写·四类语言的边界】

易混语句正确分类关键原因
TRUNCATE TABLE tDDL重定义表存储、隐式提交
DELETE FROM tDML逐行删除、可 WHERE、可回滚
CREATE INDEXDDL定义索引对象
SELECT ... FOR UPDATEDML(当前读)仍是查询语句,但加 X 锁
SET autocommit=0TCL/会话控制影响事务边界
GRANT/REVOKEDCL只改权限元数据
记忆:结构 DDL、数据 DML、权限 DCL、事务 TCL。

【记忆锚点】 「定义管结构、操纵管数据、控制管权限、事务管提交」。

【易混对比】

  • 换个问法:「TRUNCATE TABLE T 属于哪类语句?」→ 答 DDL,不是 DML;它与 DELETE FROM T 效果相似,但分类与可回滚性都不同。
  • 易混概念:DELETE(DML,可回滚,可带 WHERE)↔ TRUNCATE(DDL,隐式提交,清空全表)。

【自测】

  1. TRUNCATE TABLE t 与 DELETE FROM t(不带 WHERE)都能清空表,分类与后果差在哪? 参考答案:TRUNCATE 是 DDL——按对象结构操作、隐式提交、不能回滚、通常重置自增列;DELETE 是 DML——逐行删、可带 WHERE、可回滚,会写大量 undo/redo 并触发触发器。
  2. 只给用户查询 shop.orders 的权限,该用哪类语句?写出命令。 参考答案:DCL —— GRANT SELECT ON shop.orders TO 'app'@'10.%';(收回用 REVOKE SELECT ON shop.orders FROM 'app'@'10.%';,查看用 SHOW GRANTS FOR 'app'@'10.%';)。
  3. COMMIT、ROLLBACK、SAVEPOINT 属哪类?为什么不是 DCL? 参考答案:属 TCL(事务控制语言),管的是事务边界;DCL 只管权限的授予与回收(GRANT/REVOKE),二者别因"控制"二字混为一谈。

【选项误解】

  • A 误解来源:CREATE/ALTER/DROP 操作对象是结构,属 DDL;考生常因「DROP 会删数据」误判为 DML。
  • B 误解来源:最常见错误——把「最常写的 SQL」默认当成某一类。SELECT/INSERT/UPDATE/DELETE 全是 DML。
  • C 正确项:GRANT/REVOKE 只管权限,是 DCL 的全部成员(标准 SQL 口径)。
  • D 误解来源:COMMIT/ROLLBACK 控制事务边界,属 TCL;因与「控制」字面接近,容易被误塞进 DCL。

【知识关联】

  • 主库关联:M31(防 SQL 注入——参数化查询属应用层权限/校验延伸);主库未系统讲 SQL 语言分类。
  • 国网/408:国网考纲第 12 条「关系数据库标准语言 SQL」;408 可能考语言分类填空。
  • 面试追问:① TRUNCATE 属 DDL 还是 DML?→ DDL(隐式提交、通常不可回滚、重置自增)。② MySQL 权限体系如何落地?→ mysql.user/db/tables_priv/columns_priv 表 + FLUSH PRIVILEGES;8.0 起用角色 Role。③ GRANT 和视图如何配合做权限控制?→ 只对视图授权、不授权基表,用户只能看到视图暴露的行列。

【拓展延伸】

  • 变式问法:①「属于 DDL 的是?」→ CREATE/ALTER/DROP/TRUNCATE;②「属于 TCL 的是?」→ COMMIT/ROLLBACK/SAVEPOINT;③「GRANT SELECT ON db.v TO u@'%' 属哪类?」→ DCL。
  • 工程参数/命令:
sql
-- DCL
GRANT SELECT, INSERT ON shop.* TO 'app'@'10.%';
REVOKE INSERT ON shop.* FROM 'app'@'10.%';
SHOW GRANTS FOR 'app'@'10.%';
-- MySQL 8 角色
CREATE ROLE 'read_only';
GRANT SELECT ON *.* TO 'read_only';
GRANT 'read_only' TO 'app'@'10.%';
  • 安全实践:业务账号按最小权限授予;禁用 GRANT ALL;管理端与业务端账号分离;审计开启 general_log 或企业级审计插件。

补-07 ​

关于 SQL 查询中 WHERE 与 HAVING 子句,下列说法正确的是( )。 A. WHERE 作用于分组后的结果,HAVING 作用于分组前 B. WHERE 在分组前过滤行,不能使用聚合函数;HAVING 在分组后过滤组,可以使用聚合函数 C. 二者完全等价,可以互换使用 D. HAVING 必须与 ORDER BY 子句一起使用

答案:B

📌 考点定位: SQL 查询语句结构与执行顺序,对应国网考纲第 12 条;互联网笔试选择题高频。

【结论】 选 B。WHERE 作用于分组前的基表行、不能出现聚合函数;HAVING 作用于分组后的组、可以使用聚合函数,二者的差别源于它们在逻辑执行顺序中处于不同阶段。

【逐项辨析】

  • A WHERE 作用于分组后的结果,HAVING 作用于分组前:错。把两者的作用阶段整体说反了——WHERE 在 GROUP BY 之前,HAVING 在 GROUP BY 之后。
  • B WHERE 在分组前过滤行,不能使用聚合函数;HAVING 在分组后过滤组,可以使用聚合函数:正确。 这一表述同时命中了「执行阶段」与「能否使用聚合函数」两个判断维度。
  • C 二者完全等价,可以互换使用:错。作用阶段不同,WHERE 处理的是原始行、HAVING 处理的是分组结果,把 COUNT(*) > 5 写进 WHERE 会直接报错(注意报错阶段:语句能通过语法分析,是在语义/名称解析阶段被判定为非法使用聚合函数,MySQL 返回 ERROR 1111 (HY000): Invalid use of group function)。
  • D HAVING 必须与 ORDER BY 子句一起使用:错。HAVING 的配套子句是 GROUP BY,与 ORDER BY 没有必然关系;单独出现 HAVING 时通常隐含「整表作为一组」的语义。

【知识点】 SQL 语句的书写顺序与逻辑执行顺序并不一致,这是本题的核心:

书写位置逻辑执行顺序作用
SELECT5选取列、计算聚合值、生成别名
FROM1确定数据来源、完成连接
WHERE2对行做过滤(不能用聚合函数、不能用 SELECT 别名)
GROUP BY3按分组列把行聚成组
HAVING4对组做过滤(可以使用聚合函数)
ORDER BY6对结果排序
LIMIT7截取结果行数

由执行顺序可直接推出两条结论:

  1. WHERE 里不能用 SELECT 中定义的别名——别名在第 5 步才产生,而 WHERE 在第 2 步执行时它还不存在。
  2. 聚合函数只能出现在 HAVING 或 SELECT 中——聚合值要到第 3 步分组之后才产生,WHERE 在第 2 步执行时无组可分。

标准写法示例:

text
SELECT dept, COUNT(*) AS cnt
FROM   Student
WHERE  age > 18          -- 先筛行:年龄大于 18 的学生
GROUP  BY dept           -- 再分组:按院系分组
HAVING COUNT(*) > 5      -- 后筛组:人数超过 5 的院系
ORDER  BY cnt DESC;      -- 最后排序

【知识点扩写·为何聚合不能进 WHERE】

text
逻辑执行:
  FROM   → 读表/连接
  WHERE  → 逐行过滤(此时还没有「组」概念)
  GROUP  → 把行聚成组
  HAVING → 对组过滤(聚合值已产生)
  SELECT → 投影/别名
若 WHERE 中写 COUNT(*)>5:
  语法分析能过,但在语义/名称解析阶段被拒——聚合值在 WHERE 时刻尚不存在
MySQL 错误示例:ERROR 1111 (HY000): Invalid use of group function

书写顺序 ≠ 执行顺序 是本考点最大的误解来源,建议把执行顺序表背熟。

【记忆锚点】 「WHERE 管行、HAVING 管组;WHERE 在分组前、HAVING 在分组后」。

【易混对比】

  • 换个问法:「SELECT dept FROM Student WHERE COUNT(*) > 5 GROUP BY dept 报错的原因是什么?」→ 聚合函数出现在了 WHERE 中,违反「聚合函数只能出现在 HAVING 或 SELECT」的规则。
  • 再换问法:「WHERE age > 18 AND COUNT(*) > 1 这种写法能用吗?」→ 不能,COUNT 必须在 HAVING 中使用。
  • 与补-08、补-09 连考:WHERE / HAVING 的执行阶段常与「外连接保留行」「COUNT(*) 与 COUNT(列) 的差异」组合出题。
  • 易混概念:WHERE ↔ HAVING(行 vs 组)、GROUP BY ↔ ORDER BY(分组改变行数,排序不改变行数)。

【自测】

下列 SQL 中语法正确的是( )。 A. SELECT dept FROM Student WHERE COUNT(*) > 3 GROUP BY dept B. SELECT dept, COUNT(*) FROM Student GROUP BY dept HAVING COUNT(*) > 3 C. SELECT dept FROM Student GROUP BY dept WHERE COUNT(*) > 3 D. SELECT dept FROM Student HAVING COUNT(*) > 3 WHERE age > 18 答:B。A 把聚合函数写进了 WHERE;C 把 WHERE 写到了 GROUP BY 之后;D 中 WHERE 的位置与书写顺序均错误。(关联补-07 WHERE 与 HAVING 的执行阶段)

【选项误解】

  • A 误解来源:WHERE/HAVING 阶段对调。常见说法「HAVING 更高级应该先过滤」完全错误——执行顺序是 WHERE → GROUP BY → HAVING。
  • B 正确项:同时命中「执行阶段」与「能否用聚合」两个判据。
  • C 误解来源:认为两者都「过滤」所以可互换。COUNT(*)>5 写进 WHERE 会报错(ERROR 1111 (HY000),是语义解析阶段判定「非法使用聚合函数」,语法本身能过)。
  • D 误解来源:把 HAVING 与 ORDER BY 绑定。HAVING 的配套是 GROUP BY;ORDER BY 可独立出现。

【知识关联】

  • 主库关联:M20(Using filesort——ORDER BY 无索引时的排序代价)、M25(COUNT 口径)、M05(GROUP BY 列能否走索引避免临时表)。
  • 国网/408:国网第 12 条;408 SQL 大题常要求写「按条件筛选组」的查询,必须区分 WHERE/HAVING。
  • 面试追问:① WHERE 里为何不能用 SELECT 别名?→ 别名在 SELECT 阶段才生成,WHERE 更早执行(MySQL 有些方言宽松,但标准不允许)。② HAVING 能否用非聚合列?→ 可以,但该列必须在 GROUP BY 中或被聚合(ONLY_FULL_GROUP_BY)。③ 无 GROUP BY 的 HAVING 什么语义?→ 整表作为一组。

【拓展延伸】

  • 变式问法:①「过滤分组前的行」→ WHERE;②「过滤聚合后的组」→ HAVING;③「逻辑执行顺序排序题」→ FROM→WHERE→GROUP BY→HAVING→SELECT→ORDER BY→LIMIT。
  • 工程参数/命令:
sql
-- 标准写法
SELECT dept, COUNT(*) AS cnt
FROM student
WHERE age > 18
GROUP BY dept
HAVING COUNT(*) > 5
ORDER BY cnt DESC
LIMIT 10;

-- 查看 ONLY_FULL_GROUP_BY
SELECT @@sql_mode;
-- 临时关闭(不推荐生产)
SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION';
  • 性能提示:能在 WHERE 完成的过滤绝不下放到 HAVING——WHERE 在分组前减少行数,分组/聚合代价随之下降;索引对 WHERE 的收益也远大于 HAVING。

补-08 ​

要查询「所有学生及其选课情况,包括没有选任何课的学生」,应采用( )。 A. 内连接(INNER JOIN) B. 交叉连接(CROSS JOIN) C. 右外连接(RIGHT JOIN),以选课表为主表 D. 左外连接(LEFT JOIN),以学生表为主表

答案:D

📌 考点定位: SQL 多表连接查询,对应国网考纲第 12 条;互联网笔试选择题高频。

【结论】 选 D。要保留「没有选任何课的学生」,必须以学生表为主表做左外连接,让未匹配的从表列以 NULL 填充;内连接会把这类学生直接丢弃。

【逐项辨析】

  • A 内连接(INNER JOIN):错。内连接只返回两表中匹配成功的行,未选课的学生在选课表中没有对应记录,会被直接过滤掉,与题干「包括没有选任何课的学生」矛盾。
  • B 交叉连接(CROSS JOIN):错。交叉连接即笛卡尔积,每个学生与每条选课记录都强行组合,会产生大量无意义的错配行,而非「每个学生对应其真实选课情况」。
  • C 右外连接(RIGHT JOIN),以选课表为主表:错。右外连接保留的是右表(选课表)的全部行,即「所有选课记录」,方向正好相反,未选课的学生仍然不会出现。
  • D 左外连接(LEFT JOIN),以学生表为主表:正确。 左表(学生表)的每一行都被保留,能在选课表中匹配到的显示真实选课信息,匹配不到的选课字段填 NULL,正好实现「所有学生 + 其选课情况(含未选课)」。

【知识点】 连接查询按「是否保留未匹配行」分为内连接与外连接两大类:

连接类型保留的行未匹配侧的列典型用途
内连接 INNER JOIN两表都匹配上的行—只关心有对应关系的数据
左外连接 LEFT JOIN左表全部 + 右表匹配行右表列填 NULL保留左表全集
右外连接 RIGHT JOIN右表全部 + 左表匹配行左表列填 NULL保留右表全集
全外连接 FULL JOIN两表全部行两侧都可能填 NULL保留双方全集(MySQL 不直接支持,需用 UNION 模拟)
交叉连接 CROSS JOIN两表所有组合—笛卡尔积

判断主表的口诀是「要保留谁的全部行,谁就是主表」。在 A LEFT JOIN B 中主表是 A(左表),在 A RIGHT JOIN B 中主表是 B(右表)。

补充两点:

  1. 左外连接与右外连接可以互相改写:A LEFT JOIN B 等价于 B RIGHT JOIN A,只要交换表的顺序并把 LEFT 换成 RIGHT 即可。
  2. 过滤条件写在不同位置,语义不同:写在 ON 中只影响匹配过程,写在 WHERE 中会过滤掉 NULL 填充行,从而把外连接「退化成」内连接——这是外连接最常见的坑。
text
-- 正确:未选课学生的选课字段为 NULL,但仍被保留
SELECT s.sno, s.sname, sc.cno
FROM   Student s LEFT JOIN SC sc ON s.sno = sc.sno;

-- 危险:WHERE 过滤 cno 会剔除 NULL 行,等价于内连接
SELECT s.sno, s.sname, sc.cno
FROM   Student s LEFT JOIN SC sc ON s.sno = sc.sno
WHERE  sc.cno IS NOT NULL;

【记忆锚点】 「谁的行要全留,谁就站在 LEFT JOIN 左边;右表补 NULL」。

【易混对比】

  • 换个问法:「要查询所有课程及其选课人数,包括无人选的课程」→ 以课程表为主表做左外连接(或把选课表放在右外连接的位置),与本题主表方向相反,注意切换。
  • 再换问法:「SELECT ... FROM A LEFT JOIN B ON ... WHERE B.col = 'x' 还保留 A 的全部行吗?」→ 不保留,WHERE 会剔除 B 侧为 NULL 的行,实际等价于内连接;若要保留应把该条件写进 ON。
  • 与补-07、补-09 连考:外连接产生的 NULL 行与 COUNT(*) / COUNT(列) 的差异常组合出题——统计「每个学生的选课门数(含 0 门)」时必须用 COUNT(sc.cno),因为未选课学生的 COUNT(*) 会返回 1(整行存在),而 COUNT(sc.cno) 才返回 0。
  • 易混概念:内连接 ↔ 外连接(是否保留未匹配行)、左外连接 ↔ 右外连接(主表方向)、ON 条件 ↔ WHERE 条件(是否影响 NULL 行的保留)。

【自测】

表 Course(Cno, Cname) 与 SC(Sno, Cno) 中,要查询「所有课程及选课人数,包括没有学生选的课程」,正确的 SQL 是( )。 A. SELECT c.Cno, COUNT(*) FROM Course c JOIN SC s ON c.Cno = s.Cno GROUP BY c.Cno B. SELECT c.Cno, COUNT(s.Sno) FROM Course c LEFT JOIN SC s ON c.Cno = s.Cno GROUP BY c.Cno C. SELECT c.Cno, COUNT(*) FROM Course c LEFT JOIN SC s ON c.Cno = s.Cno GROUP BY c.Cno D. SELECT c.Cno, COUNT(s.Sno) FROM Course c RIGHT JOIN SC s ON c.Cno = s.Cno GROUP BY c.Cno 答:B。需保留课程表全集故用 LEFT JOIN,且无人选的课程要计 0 人,必须用 COUNT(s.Sno) 跳过 NULL;C 的 COUNT(*) 会把 NULL 填充行也计为 1,导致无人选的课程显示 1 人。(关联补-08 外连接 + 补-09 聚合函数对 NULL 的处理)

【选项误解】

  • A 误解来源:日常 INNER JOIN 写多了,看到「学生+选课」默认内连接,忽略题干「包括没有选任何课」。
  • B 误解来源:把「连接」一词泛化成笛卡尔积。交叉连接会产生 m×n 错配行,语义完全错误。
  • C 误解来源:知道要用外连接,但主表方向搞反。右外连接保留的是选课表全集=所有选课记录,未选课学生仍不出现。
  • D 正确项:学生表在左,LEFT JOIN 保留学生全集,未匹配选课列填 NULL。

【知识关联】

  • 主库关联:M04(JOIN 列索引)、M05(联合索引最左前缀在连接条件上的应用)、M25(COUNT 与 NULL)。
  • 国网/408:国网第 12 条;408 SQL 题「查询没有选课的学生」是经典变体(LEFT JOIN + IS NULL 或 NOT IN/NOT EXISTS)。
  • 面试追问:① 「没选课的学生」另一种写法?→ SELECT * FROM Student s WHERE NOT EXISTS (SELECT 1 FROM SC WHERE sno=s.sno)。② LEFT JOIN 的 WHERE 写 IS NOT NULL 会怎样?→ 退化为 INNER JOIN。③ 外连接 ON 与 WHERE 区别?→ ON 只影响匹配;WHERE 过滤最终结果(含 NULL 行)。

【拓展延伸】

  • 变式问法:①「所有课程及选课人数,含 0 人课」→ 课程表 LEFT JOIN 选课表 + COUNT(sc.sno);②「查询从不选课的学生」→ LEFT JOIN ... WHERE sc.sno IS NULL;③「A LEFT JOIN B」等价改写 → B RIGHT JOIN A。
  • 工程参数/命令:
sql
-- 正确:保留未选课学生
SELECT s.sno, s.sname, sc.cno
FROM student s
LEFT JOIN sc ON s.sno = sc.sno;

-- 危险:退化成内连接
SELECT s.sno, s.sname, sc.cno
FROM student s
LEFT JOIN sc ON s.sno = sc.sno
WHERE sc.cno IS NOT NULL;

-- MySQL 不直接支持 FULL OUTER JOIN,可用 UNION 模拟
SELECT * FROM a LEFT JOIN b ON a.id=b.id
UNION
SELECT * FROM a RIGHT JOIN b ON a.id=b.id;
  • 性能:外连接驱动表固定为左表时,被驱动表连接列必须有索引,否则易出现全表扫描。

补-09 ​

已知表 T 共有 10 行,列 age 中有 3 行的值为 NULL,则 SELECT COUNT(*), COUNT(age) FROM T 的结果分别是( )。 A. 7, 7 B. 10, 10 C. 10, 7 D. 7, 10

答案:C

📌 考点定位: SQL 聚合函数与 NULL 处理,国企 / 互联网笔试高频。

【结论】 选 C。COUNT(*) 统计行数,与列值是否为 NULL 无关,结果为 10;COUNT(age) 统计该列非 NULL 值的个数,跳过 3 个 NULL,结果为 7。

【逐项辨析】

  • A 7, 7:错。把 COUNT(*) 也当成了忽略 NULL 的函数,而 COUNT(*) 数的是行,3 行 NULL 同样是 3 行。
  • B 10, 10:错。把 COUNT(age) 也当成了不忽略 NULL 的函数,而带列名的 COUNT 只统计非 NULL 值。
  • C 10, 7:正确。 COUNT(*) = 全部行数 = 10;COUNT(age) = age 中非 NULL 值的个数 = 10 − 3 = 7。
  • D 7, 10:错。两个结果正好说反,把「忽略 NULL」安到了 COUNT(*) 上,把「不忽略 NULL」安到了 COUNT(age) 上。

【知识点】 SQL 聚合函数对 NULL 的处理规则可以归纳为一句话:除 COUNT(*) 外,所有聚合函数都忽略 NULL。

聚合函数是否忽略 NULL计算口径本题数据下的结果
COUNT(*)否统计行数,不管列值是否为 NULL10
COUNT(age)是统计 age 的非 NULL 值个数7
SUM(age)是非 NULL 值之和只累加 7 个值
AVG(age)是SUM(age) / 非 NULL 个数分母是 7,不是 10
MAX(age) / MIN(age)是在非 NULL 值中取极值忽略 NULL

由「忽略 NULL」可推出两个高频推论:

  1. AVG 的分母会变小。设 10 行 age 之和为 S,则 AVG(age) = S / 7 而不是 S / 10。若想按全部行数求平均,须写成 SUM(age) / COUNT(*),并注意整数除法的取整问题。
  2. COUNT(*) 与 COUNT(列) 的结果可能不同,当且仅当该列不含 NULL 时二者才相等。

关于 NULL 的判定与运算:

text
NULL 不是 0,也不是空字符串,它表示「值未知」
NULL 参与算术运算 → 结果为 NULL(如 5 + NULL = NULL)
NULL 的比较必须用 IS NULL / IS NOT NULL,不能用 = NULL
聚合函数 SUM / AVG / MAX / MIN / COUNT(列) 在计算前先剔除 NULL
若一组内所有值都是 NULL:COUNT(列) 返回 0,SUM / AVG / MAX / MIN 返回 NULL

【完整推导·聚合与 NULL】

text
规则:除 COUNT(*) 外,所有聚合函数计算前先剔除 NULL
推论链:
  COUNT(*) 计行     → 与 NULL 无关
  COUNT(col) 计非空 → 与 NULL 负相关
  SUM(col)  只加非空 → NULL 贡献 0,但 SUM 结果仍可能非 0
  AVG(col)=SUM/COUNT(col) → 分母自动排除 NULL → 结果偏大(相对按总行数平摊)
反例验证:
  表 T: age = {20, NULL, 30, NULL, NULL, 40}
  COUNT(*)=6, COUNT(age)=3
  SUM=90, AVG=90/3=30
  若误用 SUM/COUNT(*)=90/6=15,会得到完全错误的平均值

【记忆锚点】 「COUNT(*) 数行不挑食,COUNT(列) 挑掉 NULL;AVG 忽略 NULL,所以分母变小、结果偏大」。

【易混对比】

  • 换个问法:「表 T 共 10 行,sal 列有 4 行为 NULL,则 SUM(sal) 与 AVG(sal) 的分母分别是多少?」→ SUM 没有分母概念,只累加 6 个非 NULL 值;AVG(sal) 的分母是 6。
  • 再换问法:「SELECT AVG(age) FROM T 与 SELECT SUM(age) / COUNT(*) FROM T 结果是否相同?」→ 通常不同;按本题数据(10 行、3 行为 NULL)前者分母是非空个数 7、后者分母是总行数 10,若 SUM(age)=210 则 210/7=30 > 210/10=21,后者结果更小。
  • 与补-08 连考:统计「每个学生的选课门数(含 0 门)」时,左外连接下必须用 COUNT(sc.cno);若误用 COUNT(*),未选课的学生会被算成 1 门。
  • 易混概念:COUNT(*) ↔ COUNT(列)(行数 vs 非 NULL 值个数)、= NULL ↔ IS NULL(前者结果恒为 UNKNOWN,永远查不到数据)。

【自测】

表 T 共 20 行,列 score 中有 5 行为 NULL,其余值之和为 300。则 SELECT AVG(score), COUNT(*), COUNT(score) FROM T 的结果是( )。 A. 15, 20, 15 B. 20, 20, 15 C. 20, 20, 20 D. 15, 15, 20 答:B。AVG(score) = 300 / (20 − 5) = 20,分母是非 NULL 个数 15;COUNT(*) = 20;COUNT(score) = 15。(关联补-09 聚合函数与 NULL 处理)

【选项误解】

  • A 误解来源:把 COUNT(*) 也理解成「忽略 NULL」。COUNT(*) 数的是行,整行 NULL 也是行。
  • B 误解来源:以为所有 COUNT 都不挑值。带列名的 COUNT(col) 明确跳过 NULL。
  • C 正确项:10 行 → COUNT(*)=10;3 个 NULL → COUNT(age)=7。
  • D 误解来源:两个结果对调——把「忽略 NULL」安到了 COUNT(*) 上。

【知识关联】

  • 主库关联:M25(COUNT 函数正确说法——COUNT(*)/COUNT(1)/COUNT(列) 差异与性能)、补-08(外连接 + COUNT(sc.col) 才能计 0)。
  • 国网/408:国网 SQL 部分;笔试计算题常直接给 NULL 行数要求写 COUNT 结果。
  • 面试追问:① COUNT(*)、COUNT(1)、COUNT(id) 哪个最快?→ InnoDB 优化器通常等价,都走最小可用索引;COUNT(列) 若列可空则可能更慢且结果更小。② 为何 AVG 分母不是总行数?→ AVG = SUM/COUNT(列),忽略 NULL。③ 大表 COUNT(*) 慢怎么办?→ 近似统计(information_schema.TABLES.TABLE_ROWS)、维护计数表、或缓存。

【拓展延伸】

  • 变式问法:①「10 行中 3 NULL,SUM(age)/AVG(age) 分母?」→ SUM 无分母;AVG 分母=7;②「COUNT(DISTINCT age)?」→ 去重且忽略 NULL,最大 7;③「全 NULL 列」→ COUNT(列)=0,SUM/AVG/MAX/MIN 返回 NULL。
  • 完整计算推导:
text
已知:总行数 N=10,age 非空个数 = 10-3=7
COUNT(*)     = N = 10
COUNT(age)   = 非空个数 = 7
若 SUM(age)=210,则 AVG(age)=210/7=30(不是 210/10=21)
SUM(age)/COUNT(*) = 210/10=21(若要按总行数平摊需显式这样写)
  • 工程参数/命令:
sql
SELECT COUNT(*), COUNT(age), SUM(age), AVG(age) FROM t;
-- 近似行数(MySQL)
SELECT TABLE_ROWS FROM information_schema.TABLES
 WHERE TABLE_SCHEMA='db' AND TABLE_NAME='t';
-- InnoDB 精确 COUNT 代价:需扫索引
EXPLAIN SELECT COUNT(*) FROM t;

补-10 ​

关于数据库视图(VIEW),下列说法正确的是( )。 A. 视图是物理存储的数据副本,占用与基表相同的存储空间 B. 视图是从一个或多个基表导出的虚表,只保存定义不保存数据,可简化查询并用于权限控制 C. 任何视图都可以执行 INSERT / UPDATE / DELETE D. 视图一旦创建,其定义永远不可修改

答案:B

📌 考点定位: SQL——视图的概念与作用,对应国网考纲第 12 条,国企笔试常考。

【结论】 选 B。视图是从一个或多个基表导出的虚表,数据库中只保存其查询定义、不保存数据,查询时才动态从基表生成结果,因此可用于简化查询与实现权限控制。

【逐项辨析】

  • A 视图是物理存储的数据副本,占用与基表相同的存储空间:错。视图不存储数据,只保存一条查询定义,因此不会占用与基表相同的存储空间。
  • B 视图是从一个或多个基表导出的虚表,只保存定义不保存数据,可简化查询并用于权限控制:正确。 「虚表」「只存定义」是视图的本质,「简化查询」「权限控制」是它的两大典型作用。
  • C 任何视图都可以执行 INSERT / UPDATE / DELETE:错。视图的可更新性受条件约束,含聚合函数、DISTINCT、GROUP BY、多表连接、表达式列等的视图通常不可更新,说「任何视图都可以」是错的。
  • D 视图一旦创建,其定义永远不可修改:错。视图定义可以用 ALTER VIEW 修改,也可以用 CREATE OR REPLACE VIEW 重建。

【知识点】 视图是从一个或几个基本表(或其它视图)导出的表,它是虚表——数据库中只存放视图的定义,不存放对应的数据。

对比项基本表视图
是否存储数据存储不存储,只存定义
数据来源自身基表(或其它视图)
是否占用数据空间占用不占用数据空间,仅定义存入数据字典
能否建立索引可以不可以(物化视图除外,MySQL 不原生支持)
增删改支持受可更新性条件限制

视图的三大作用:

  1. 简化操作:把复杂的多表连接、聚合封装成一个视图,用户只需查询视图。
  2. 逻辑数据独立性:基表结构变化时,只需重定义视图,用户程序不受影响。
  3. 安全保护:通过视图只暴露部分行或部分列,配合 GRANT 把访问权限限定在视图上,用户看不到基表的其它数据。

视图的可更新性判据是「能否与基表行一一对应」:

text
通常不可更新的视图:
  含聚合函数(SUM / COUNT / AVG / MAX / MIN)
  含 DISTINCT、GROUP BY、HAVING
  由多表连接产生(含 JOIN)
  含表达式列、子查询、UNION
通常可更新的视图:
  行列子集视图(从单个基表选出部分行、部分列,未做任何加工)

相关语句:

text
CREATE VIEW v_cs AS SELECT sno, sname FROM Student WHERE sdept = 'CS';
ALTER  VIEW v_cs AS SELECT sno, sname, age FROM Student WHERE sdept = 'CS';
DROP   VIEW v_cs;
SHOW   CREATE VIEW v_cs;   -- 查看视图定义

补充:MySQL 中若视图定义与基表之间的对应关系可被推导,则支持 INSERT / UPDATE / DELETE;使用 WITH CHECK OPTION 可以限制更新后的行必须仍满足视图的 WHERE 条件。

【知识点扩写·视图与三级模式】

text
用户程序
   ↓ 使用
外模式(视图/用户视图)     ← 逻辑独立性的保护对象
   ↑ 外模式/模式映射
模式(基表逻辑结构)
   ↑ 模式/内模式映射
内模式(物理存储)
视图创建:在数据字典中插入一条 SELECT 定义
视图查询:解析时把视图名替换为定义中的子查询,再优化执行
因此:基表数据变化 → 视图结果立即变化;基表加列 → 视图列不变(逻辑独立)

【记忆锚点】 「视图只占定义、不占数据;能更新的是行列子集视图,带聚合和多表连接的就别想改」。

【易混对比】

  • 换个问法:「SELECT * FROM v_cs 每次执行都会重新从基表取数吗?」→ 是。视图是虚表,每次查询都按定义实时生成结果,基表数据一变视图结果就变,不存在「数据快照」。
  • 再换问法:「下列哪个视图一定可以执行 UPDATE?」→ 从单表选出部分行列、无聚合、无 DISTINCT 的行列子集视图。
  • 与补-06 连考:视图的创建与删除属 DDL(CREATE VIEW / DROP VIEW),而通过视图操作数据属 DML,两者常被混考。
  • 易混概念:视图 ↔ 基本表(虚表 vs 实表)、视图 ↔ 物化视图(前者不存数据、后者存数据,MySQL 不原生支持物化视图)。

【自测】

下列关于视图的说法,错误的是( )。 A. 视图可以建立在基本表之上,也可以建立在其它视图之上 B. 视图可以用于实现权限控制,只向用户暴露部分行与列 C. 视图中的数据会随基表数据的更新而自动变化 D. 对视图执行 UPDATE 时,被更新的数据会存储到视图中 答:D。视图不存储数据,对视图的更新实际被转换为对基表的更新,数据存放在基表而不是视图。(关联补-10 视图的本质)

【选项误解】

  • A 误解来源:把视图当成「快照表」或「复制表」。视图只存定义,不存数据。
  • B 正确项:虚表 + 只存定义 + 简化查询 + 权限控制,四要素齐全。
  • C 误解来源:以为视图和表完全同构可任意增删改。含聚合/连接/GROUP BY/DISTINCT/表达式列的视图通常不可更新。
  • D 误解来源:以为定义冻结。ALTER VIEW / CREATE OR REPLACE VIEW 都可改定义。

【知识关联】

  • 主库关联:补-01(外模式/逻辑独立性——视图是外模式的工程载体);主库 M 无直接视图题。
  • 国网/408:国网第 12 条 SQL;408 可能考视图的更新性与安全性。
  • 面试追问:① 视图能建索引吗?→ 普通视图不能;物化视图才存数据(MySQL 不原生支持,Oracle/PG 有)。② 视图如何做权限隔离?→ GRANT 只授视图、不授基表。③ WITH CHECK OPTION 作用?→ 更新后的行必须仍满足视图 WHERE 条件,否则拒绝。

【拓展延伸】

  • 变式问法:①「视图是否占用与基表相同空间?」→ 否,只占数据字典中的定义;②「哪种视图一定可 UPDATE?」→ 单表行列子集、无聚合无 DISTINCT;③「视图数据会随基表变吗?」→ 会,每次查询实时生成。
  • 工程参数/命令:
sql
CREATE OR REPLACE VIEW v_cs AS
  SELECT sno,sname FROM student WHERE sdept='CS'
  WITH CHECK OPTION;
SHOW CREATE VIEW v_cs;
-- 可更新性粗判
SELECT TABLE_NAME, IS_UPDATABLE
  FROM information_schema.VIEWS WHERE TABLE_NAME='v_cs';
DROP VIEW v_cs;
  • 物化视图对照:真正把结果存成物理表,查询快但要维护刷新(定时/触发);MySQL 需用触发器+表或第三方方案模拟。

补-11 ​

要查询列 email 取值为空的记录,正确的 WHERE 子句是( )。 A. WHERE email IS NULL B. WHERE email = NULL C. WHERE email = '' D. WHERE email != NULL

答案:A

📌 考点定位: SQL 三值逻辑与 NULL 判断,国企/互联网笔试高频。

【结论】 判空必须用 IS NULL;= NULL 语法不报错但结果恒为 UNKNOWN,一行也查不出来 —— 选 A。

【逐项辨析】

  • A WHERE email IS NULL:正确。 IS NULL 是 SQL 专为 NULL 设计的一元判定运算符,返回值只有 TRUE / FALSE,永远不会是 UNKNOWN。
  • B WHERE email = NULL:错。错在把 NULL 当成普通值去比较 —— = 遇 NULL 结果恒为 UNKNOWN,而 WHERE 只保留结果为 TRUE 的行。
  • C WHERE email = '':错。错在把 NULL 与空字符串混为一谈 —— '' 是「已知的空值」,是一个确定的字符串值,语义是「填了,但填的是空」。
  • D WHERE email != NULL:错。与 B 同因 —— !=(等价写法 <>)遇 NULL 同样返回 UNKNOWN,恒不成立,看起来与 B 相反、实则一样查不到行。

【知识点】 SQL 的布尔域是三值逻辑:{TRUE, FALSE, UNKNOWN}。NULL 的含义是「值未知」,它不是空串、不是 0、也不是「没有值」,而是「不知道」。

核心规则:任何与 NULL 进行的比较或算术运算,结果都是 UNKNOWN 或 NULL。

表达式结果说明
NULL = NULLUNKNOWNNULL 与自身比较也不为真
NULL != NULLUNKNOWN同理,!= 也不成立
NULL = 1UNKNOWN未知值与确定值无法比较
1 + NULLNULL算术运算被 NULL 传染
NULL IS NULLTRUEIS 是唯一能「看见」NULL 的运算符
NULL IS NOT NULLFALSE与上一行互为反面

WHERE 子句的语义是「只保留条件为 TRUE 的行」——UNKNOWN 与 FALSE 待遇相同,都被过滤掉。因此 email = NULL 与 email != NULL 这对「看起来对称」的写法,实际结果都是 0 行,这是最阴险的陷阱。

三处补充口径:

  1. GROUP BY 与 DISTINCT 会把所有 NULL 归入同一组(分组用的是「是否相同」而非 =),这与「NULL 互不相等」并不矛盾。
  2. UNIQUE 约束允许多行同时为 NULL(因为 NULL 互不相等)。
  3. 标准 SQL 的 IS DISTINCT FROM 把 NULL 当普通值比较,但 MySQL 不支持该语法,MySQL 用 NULL 安全等于 <=> 替代:NULL <=> NULL 返回 TRUE。

【记忆锚点】 「NULL 跟谁比都是 UNKNOWN,判空只能用 IS NULL」——= NULL 不会报错,但会安静地返回空结果集。

【易混对比】

  • NULL vs '':NULL 是「未知」,'' 是「已知的空值」。COUNT(列) 计入 ''、跳过 NULL(见补-09)。
  • IS NULL vs <=>:IS NULL 是单目判定;<=> 是 MySQL 的 NULL 安全等值比较(1 <=> 1 为 TRUE,NULL <=> NULL 也为 TRUE)。
  • UNKNOWN vs FALSE:在 WHERE 里效果一样,但在 NOT 下不同 —— NOT UNKNOWN 仍是 UNKNOWN,所以 WHERE NOT (email = NULL) 同样查不到任何行。
  • 换问法:若题目问「WHERE email IS NOT NULL 能否查到空字符串那行」,答案是能 —— '' 不是 NULL,会被判定为 TRUE。

【自测】 表 T 有 10 行,其中 4 行的 email 为 NULL、1 行的 email 为 ''。分别执行 SELECT COUNT(*) FROM T WHERE email IS NULL 与 SELECT COUNT(*) FROM T WHERE email = NULL,结果各是多少?

答:4 与 0。IS NULL 正确匹配 4 行;= NULL 恒为 UNKNOWN,返回 0 行。若把条件换成 email IS NOT NULL 则为 6 行(含 '' 那行)。与补-09 连考;国企/互联网笔试高频。

【选项误解】

  • A 正确项:IS NULL 是唯一能判定 NULL 的标准运算符。
  • B 误解来源:最经典误解——把 NULL 当普通值用 = 比较。= 遇 NULL 结果为 UNKNOWN,WHERE 只保留 TRUE。
  • C 误解来源:把 NULL 与空字符串混为一谈。'' 是「已知的空值」,语义完全不同。
  • D 误解来源:与 B 对称的错误。!= NULL 同样得 UNKNOWN,结果也是 0 行。

【知识关联】

  • 主库关联:补-09(COUNT 忽略 NULL);主库未系统讲三值逻辑。
  • 国网/408:国网 SQL;408 数据库 NULL 语义是常见陷阱题。
  • 面试追问:① MySQL 的 NULL 安全比较?→ <=>,NULL<=>NULL 为 TRUE;标准 SQL 是 IS NOT DISTINCT FROM。② UNIQUE 约束允许多个 NULL 吗?→ MySQL InnoDB 允许(NULL 互不相等)。③ GROUP BY 如何处理 NULL?→ 所有 NULL 归为同一组。

【拓展延伸】

  • 变式问法:①「查非空」→ IS NOT NULL;②「NULL<=>NULL」→ TRUE;③「WHERE NOT (email=NULL) 能查到吗?」→ 不能,NOT UNKNOWN 仍 UNKNOWN。
  • 工程参数/命令:
sql
SELECT * FROM t WHERE email IS NULL;
SELECT * FROM t WHERE email <=> NULL;     -- MySQL NULL 安全等于
SELECT COUNT(*) FROM t WHERE email = NULL; -- 恒 0,且不报错(最坑)
-- 建表时的防御
ALTER TABLE t MODIFY email VARCHAR(64) NOT NULL DEFAULT '';
  • 工程取舍:业务上「无邮箱」更推荐 DEFAULT ''(空串)还是 NULL?→ 团队规范统一即可;若用 NULL,查询与唯一约束行为更「学术正确」但更易踩坑;金额/数量字段建议 NOT NULL + 默认 0。

补-12 ​

设关系 R(学号, 课程号, 成绩, 课程名),主键为 (学号, 课程号),且存在函数依赖「课程号 → 课程名」。则关系 R( )。 A. 满足 3NF,因为不存在传递函数依赖 B. 满足 2NF,因为所有非主属性都完全依赖于主键 C. 满足 1NF 且满足 2NF,只需再消除传递依赖即可达到 3NF D. 最高只满足 1NF,因为存在非主属性对主键的部分函数依赖

答案:D

📌 考点定位: 范式判定——部分函数依赖,对应国网考纲第 11 条,国企笔试必考。

【结论】 非主属性「课程名」只依赖主键的一部分(课程号)→ 部分函数依赖 → 违反 2NF → 最高只满足 1NF。选 D。

【逐项辨析】

  • A 满足 3NF、不存在传递依赖:错。错在跳跃判定 —— 连 2NF 都没满足,根本走不到 3NF(范式是层层递进的)。
  • B 满足 2NF、所有非主属性完全依赖主键:错。错在「完全依赖」这个字眼 —— 事实恰好相反,「课程名」是部分依赖。
  • C 满足 1NF 且满足 2NF,只需再消除传递依赖:错。错在「已满足 2NF」这个错误前提上,前提不成立,后半句再对也无意义。
  • D 最高只满足 1NF,因为存在非主属性对主键的部分函数依赖:正确。 主键是复合键 (学号, 课程号),而 课程号 → 课程名 说明「课程名」不需要完整的复合主键就能唯一确定。

【知识点】 判范式是逐级递进的,顺序不能跳:

范式要求违反时的典型特征
1NF属性不可再分出现「多值字段」(如一个格子存多个电话)
2NF消除非主属性对候选键的部分依赖复合主键下,某属性只依赖其中一部分
3NF再消除非主属性对候选键的传递依赖X → Y → Z 且 Y ↛ X
BCNF消除主属性对候选键的部分 / 传递依赖决定因素不是超键

本题判定链条:主键 (学号, 课程号) 是复合键 → 存在 课程号 → 课程名,即「课程名」只依赖复合键的一半 → 这是部分函数依赖 → 违反 2NF → 最高只到 1NF。

规范化分解:拆成 R1(学号, 课程号, 成绩) 与 R2(课程号, 课程名),两者都达到 3NF,且分解既无损连接又保持函数依赖。

一个必须记住的前提:单属性主键的表天然满足 2NF —— 单属性主键没有「真子集」可依赖,部分依赖无从谈起。所以 2NF 的坑只出现在复合主键上。

【完整推导·部分依赖与 2NF】

text
定义回顾:
  完全函数依赖:X→Y 且 X 的任何真子集都不能决定 Y
  部分函数依赖:存在 X 的真子集 X′,X′→Y
  2NF:消除非主属性对候选键的部分依赖
本题:
  X=(学号,课程号),Y=课程名,X′=课程号
  课程号→课程名 成立 ⇒ 部分依赖 ⇒ 违反 2NF
为何不是传递依赖?
  传递需要:X→Z→Y 且 Z↛X 且 Y 非主属性
  本题课程名直接由课程号决定,没有「绕弯」的中间非主属性链条
分解验证矩阵:
| 分解 | 主键 | 非主属性 | 部分依赖? | 传递依赖? | 范式 |
| R1(学号,课程号,成绩) | (学号,课程号) | 成绩 | 无 | 无 | 3NF |
| R2(课程号,课程名) | 课程号 | 课程名 | 不可能(单属性键) | 无 | 3NF |

【记忆锚点】 「先看部分依赖(2NF),再看传递依赖(3NF)」——判范式像爬楼梯,2NF 没过,3NF 免谈。

【易混对比】

  • 部分依赖 vs 传递依赖:部分依赖是「只靠主键的一半」,传递依赖是「绕了一道弯(A → B → C)」。前者卡 2NF,后者卡 3NF。
  • 2NF vs 3NF:2NF 只可能被复合主键违反;3NF 任何主键都可能违反。
  • 非主属性 vs 主属性:2NF、3NF 只管非主属性;BCNF 连主属性一起管(见补-13)。
  • 换问法:若题干把依赖改成「学号 → 姓名」(学号也是主键的一部分),仍是部分依赖;若改成「学号 → 系号、系号 → 系名」,则变成传递依赖(卡 3NF)。一字之差,卡在不同关卡。

【自测】 关系 R(学号, 姓名, 系号, 系名),主键为单属性「学号」,存在「学号 → 系号」「系号 → 系名」。R 最高满足第几范式?

答:2NF。主键是单属性,不存在部分依赖(满足 2NF);但「系号 → 系名」构成传递依赖,违反 3NF。与补-13、补-14 连考;国网真题库同型题。

【选项误解】

  • A 误解来源:跳跃判定——未先检查 2NF 就直奔 3NF。范式必须逐级满足。
  • B 误解来源:把「部分依赖」说成「完全依赖」,与事实相反。
  • C 误解来源:错误前提「已满足 2NF」;后半句再对也无效。
  • D 正确项:复合主键下存在「课程号→课程名」,非主属性课程名只依赖主键一部分 → 违反 2NF → 最高 1NF。

【知识关联】

  • 主库关联:主库无范式题(已知缺口);反范式化与 M30 分库分表、M04 覆盖索引设计相关——工程上常为性能做可控冗余。
  • 国网/408:国网第 11 条核心;408 数据库大题必考范式判断 + 分解。
  • 面试追问:① 单属性主键还需要检查 2NF 吗?→ 不需要,天然满足(无真子集可部分依赖)。② 反范式化怎么落地?→ 适当冗余、汇总表、宽表,用缓存/补偿保持最终一致。③ 本题如何分解?→ R1(学号,课程号,成绩) + R2(课程号,课程名),无损且保持 FD。

【拓展延伸】

  • 变式问法:①「存在部分依赖 → 违反(2NF)」;②「存在传递依赖 → 违反(3NF)」;③「单属性主键 + 传递依赖 → 最高 2NF」。
  • 完整判定推导:
text
关系:R(学号, 课程号, 成绩, 课程名)
候选键:先求闭包。学号⁺ 含成绩?不一定;课程号⁺ 含课程名,但推不出学号成绩
       (学号,课程号)⁺ = {学号,课程号,成绩,课程名} → 是候选键(唯一极小键)
非主属性:成绩、课程名
FD:
  (学号,课程号) → 成绩   完全依赖(少任何一半都定不了成绩)
  课程号 → 课程名        部分依赖(课程名只需主键的一半)
判定:
  1NF:属性原子 ✓
  2NF:存在非主属性对候选键的部分依赖 ✗
结论:最高 1NF
分解:
  R1(学号,课程号,成绩):主键(学号,课程号),无部分依赖,且无传递 → 3NF
  R2(课程号,课程名):主键课程号,课程名完全依赖 → 3NF
  分解无损:R1∩R2=课程号,课程号→课程名 在 R2 中
  保持函数依赖:原 FD 均被 R1 或 R2 保持
  • 工程提醒:笔试按范式答「最高 1NF」;实际业务中选课表常故意冗余课程名以减 JOIN,属反范式,需应用或触发器同步。

补-13 ​

关于 3NF 与 BCNF 的关系,下列说法正确的是( )。 A. BCNF 的约束条件比 3NF 更宽松 B. 满足 BCNF 的关系不一定满足 3NF C. 满足 BCNF 的关系一定满足 3NF,但满足 3NF 的关系不一定满足 BCNF D. 二者在定义上完全等价

答案:C

📌 考点定位: 范式——3NF 与 BCNF 的层级关系,对应国网考纲第 11 条。

【结论】 BCNF 比 3NF 更严,满足 BCNF 必然满足 3NF,反之不成立。选 C。

【逐项辨析】

  • A BCNF 的约束比 3NF 更宽松:错。错在「宽松」二字 —— BCNF 是在 3NF 之上加强约束,只能说更严。
  • B 满足 BCNF 的关系不一定满足 3NF:错。错在把蕴含方向说反 —— 层级是 1NF ⊂ 2NF ⊂ 3NF ⊂ BCNF,BCNF 是 3NF 的真子集。
  • C 满足 BCNF 一定满足 3NF,满足 3NF 不一定满足 BCNF:正确。 3NF 只管「非主属性」,BCNF 连「主属性」也管,条件更强,故前者成立、后者不成立。
  • D 二者定义上完全等价:错。错在「等价」二字 —— 等价意味着互相蕴含,而「3NF ⇒ BCNF」并不成立。

【知识点】 两个范式的定义对比(设 R 的函数依赖集为 F):

范式判定条件约束对象消除的依赖
2NF无非主属性对候选键的部分依赖仅非主属性部分依赖
3NF对每个非平凡依赖 X → Y,X 是超键 或 Y 是主属性仅非主属性部分 + 传递依赖
BCNF对每个非平凡依赖 X → Y,X 必为超键全部属性部分 + 传递依赖(含主属性)

推导「BCNF ⇒ 3NF」:设 R 满足 BCNF,任取非平凡依赖 X → Y,则 X 是超键 —— 这恰好满足 3NF 的「X 是超键或 Y 是主属性」(前半支成立即可),故 R 必然满足 3NF。方向不可逆。

反向不成立的标准反例: 关系 R(学生, 课程, 教师),语义为「每个教师只教一门课,每门课可由多位教师讲授」,候选键为 (学生, 课程) 与 (学生, 教师)。

  • 函数依赖:(学生, 课程) → 教师、(学生, 教师) → 课程、教师 → 课程。
  • 3NF 判定:前两个依赖的左部是候选键(超键);第三个依赖 教师 → 课程 左部虽非超键,但右部「课程」是主属性 → 满足 3NF。
  • BCNF 判定:教师 → 课程 中「教师」不是超键 → 违反 BCNF。

这个反例正好说明两者的分界:3NF 只要求「非主属性别出问题」,BCNF 要求「连主属性也不能被非超键决定」。

补充一条实践口径:3NF 分解可以做到既无损连接又保持函数依赖;BCNF 分解只能保证无损连接,不保证保持函数依赖。这正是实际设计中 3NF 仍被大量采用的原因。

【完整推导·BCNF ⇒ 3NF】

text
3NF 形式化:对 R 中每个非平凡 FD X→Y,
  或者 X 是超键,或者 Y 是主属性(含于任一候选键)
BCNF 形式化:对 R 中每个非平凡 FD X→Y,X 必为超键
证明 BCNF⇒3NF:
  设 R 满足 BCNF,任取非平凡 X→Y
  由 BCNF,X 是超键
  则 3NF 条件「X 是超键 OR Y 是主属性」的前半支已成立
  故 R 满足 3NF
逆命题不成立:见「教师→课程」反例
集合关系:BCNF ⊂ 3NF ⊂ 2NF ⊂ 1NF(真子集)
分解性质对比:
| 范式分解 | 无损连接 | 保持 FD |
| 3NF 合成法 | ✓ | ✓ |
| BCNF 分解 | ✓ | 不一定 |

【记忆锚点】 「3NF 只管非主属性,BCNF 连主属性一起管;3NF ⊃ BCNF」。

【易混对比】

  • 3NF vs BCNF:前者允许「主属性传递依赖于候选键」,后者不允许。判 BCNF 只看一句话 —— 每个决定因素是不是超键。
  • BCNF vs 4NF:BCNF 处理函数依赖;4NF 再处理多值依赖(消除非平凡多值依赖)。
  • BCNF 分解 vs 3NF 分解:BCNF 无损连接但不一定保持函数依赖;3NF 两者都能保证。
  • 换问法:若题目问「满足 3NF 是否一定满足 BCNF」,答案是不一定(这正是本题 C 项的后半句)。

【自测】 关系 R(学生, 课程, 教师),满足 教师 → 课程,候选键为 (学生, 课程) 与 (学生, 教师)。R 最高满足第几范式?

答:3NF。教师 → 课程 的决定因素「教师」不是超键,故违反 BCNF;但右部「课程」是主属性,3NF 允许。与补-12、补-14 连考;国网真题库同型题。

【选项误解】

  • A 误解来源:把「更严」理解成「更宽松」。BCNF 在 3NF 上加强了条件。
  • B 误解来源:蕴含方向说反。层级是 1NF⊂2NF⊂3NF⊂BCNF,BCNF⇒3NF 成立,反向不成立。
  • C 正确项:BCNF 满足必满足 3NF;3NF 不一定 BCNF(反例:教师→课程 且 教师非超键)。
  • D 误解来源:认为同义。若等价则互相蕴含,与标准结论矛盾。

【知识关联】

  • 主库关联:无直接对应(原理层缺口);工程上表设计常停在 3NF+可控反范式。
  • 国网/408:国网第 11 条;408 常考「判断到 BCNF/给出反例」。
  • 面试追问:① BCNF 分解一定能保持 FD 吗?→ 不一定,只能保证无损连接;3NF 分解可同时无损+保 FD。② 为何工程常用 3NF 而非强推 BCNF?→ 3NF 在无损与保依赖间更平衡,更新异常已大幅减少。③ 4NF 相对 BCNF 多处理什么?→ 多值依赖。

【拓展延伸】

  • 变式问法:①「满足 BCNF 是否一定 3NF?」→ 一定;②「满足 3NF 是否一定 BCNF?」→ 不一定;③「判 BCNF 的一句话」→ 每个非平凡 FD 的决定因素都是超键。
  • 完整反例推导:
text
R(学生,课程,教师)
业务:每位教师只教一门课;每门课可多位教师;每位学生可选多门课/多位教师
FD:
  (学生,课程)→教师
  (学生,教师)→课程
  教师→课程
候选键:(学生,课程) 与 (学生,教师)
主属性:学生、课程、教师(都在某个候选键中)
3NF 判定(对每个非平凡 FD,要求 X 是超键 OR Y 是主属性):
  (学生,课程)→教师:X 是超键 ✓
  (学生,教师)→课程:X 是超键 ✓
  教师→课程:X=教师 不是超键;但 Y=课程 是主属性 ✓
  ⇒ 满足 3NF
BCNF 判定(要求每个 X 都是超键):
  教师→课程:教师 不是超键 ✗
  ⇒ 违反 BCNF
结论:3NF 但非 BCNF
  • 工程对照:若把「教师→课程」拆成 Teacher(教师,课程) 与 SC(学生,教师),可到 BCNF,但查询「学生-课程」需要多一次连接——性能与规范化的权衡。

补-14 ​

若 X → Y、Y → Z,且 Y 不能决定 X(Y ↛ X),则称 X → Z 为( )。 A. 部分函数依赖 B. 传递函数依赖 C. 多值依赖 D. 平凡函数依赖

答案:B

📌 考点定位: 函数依赖的分类,范式判定的理论基础,对应国网考纲第 11 条。

【结论】 X → Y、Y → Z 且 Y ↛ X,中间「绕了一道弯」,称 X → Z 为传递函数依赖。选 B。

【逐项辨析】

  • A 部分函数依赖:错。错在「部分」这个字眼 —— 部分依赖的判据是「存在 X 的真子集 X′ 使 X′ → Y」,与本题的两段链条无关。
  • B 传递函数依赖:正确。 题干给出的正是 X → Y、Y → Z 且 Y 不能反过来决定 X 的标准三段式。
  • C 多值依赖:错。错在「多值」二字 —— 多值依赖记作 X ↠ Y,描述的是「一组值独立于另一组值」,属 4NF 范畴,不是函数依赖的链式形式。
  • D 平凡函数依赖:错。错在「平凡」二字 —— 平凡依赖的判据是「Y ⊆ X」(被决定属性是决定属性的子集,如 AB → A),本题的 Y、Z 与 X 无包含关系。

【知识点】 函数依赖 X → Y 的定义:对 R 的任意两个元组,若它们在 X 上的取值相同,则它们在 Y 上的取值也必然相同(X 函数决定 Y)。

四类依赖的定义与判据:

依赖类型形式化判据判定关键影响的范式
平凡函数依赖X → Y 且 Y ⊆ X右部是左部的子集恒成立,不影响范式
非平凡函数依赖X → Y 且 Y ⊄ X右部含左部之外的属性需具体判定
完全函数依赖X → Y 且不存在 X 真子集能决定 Y必须靠整个 X2NF 要求非主属性完全依赖
部分函数依赖存在 X 的真子集 X′ 使 X′ → Y靠 X 的一部分就够违反 2NF
传递函数依赖X → Y、Y → Z 且 Y ↛ X绕了一道弯,中间属性不能反向决定违反 3NF
多值依赖X ↠ Y一组值独立对应另一组值违反 4NF

推导「为什么 Y ↛ X 这个限定不可省」:若 Y → X 也成立,则 X 与 Y 互相决定(X ↔ Y),此时 Z 其实是被 Y(等价于 X)直接决定的,并没有「绕过中间属性」,故不构成传递依赖。所以题干特意强调「Y 不能决定 X(Y ↛ X)」——这句话是判定的必要条件,不是废话。注意别把它和「Y ⊄ X(右部不在左部之内,用来区分平凡依赖)」混为一谈:那是另一个条件。

由此得到 3NF 的完整表述:非主属性既不部分依赖、也不传递依赖于候选键。两条否定,一条对应 2NF、一条对应 3NF。

【完整推导·依赖分类速查】

类型形式卡哪个范式例
平凡X→Y, Y⊆X不影响AB→A
非平凡X→Y, Y⊄X需判定A→B
完全任意真子集不能决定 Y2NF 要求(学号,课号)→成绩
部分某真子集可决定 Y违反 2NF课程号→课程名
传递X→Y→Z, Y↛X违反 3NF学号→系号→系名
多值X↠Y违反 4NF课程↠教材(一课多教材独立于教师)
判定口诀:先看真子集(部分),再看有没有弯(传递),最后看多值独立(4NF)。

【记忆锚点】 「部分依赖靠「一半」(真子集),传递依赖绕「一道弯」(A → B → C)」。

【易混对比】

  • 部分依赖 vs 传递依赖:前者是「主键只用了一半」,后者是「中间隔了一层」。前者卡 2NF,后者卡 3NF。
  • 传递依赖 vs 多值依赖:前者是函数依赖的链式组合(→),后者是 ↠,描述独立的多值对应,属 4NF。
  • 平凡依赖 vs 非平凡依赖:唯一判据是「右部是否为左部的子集」。
  • 换问法:若题干改成「X → Y 且存在 X 的真子集 X′ → Y」,答案就变成部分函数依赖;若改成「Y ⊆ X」,答案是平凡函数依赖。

【自测】 关系 R(学号, 系号, 系主任),有 学号 → 系号、系号 → 系主任,且多个学生共享同一系号(即 系号 ↛ 学号)。学号 → 系主任 属于哪种依赖?R 最高满足第几范式?

答:传递函数依赖;主键为单属性「学号」,无部分依赖(满足 2NF),但存在传递依赖,故最高 2NF。与补-12、补-13 连考。

【选项误解】

  • A 误解来源:与「部分依赖」混淆。部分依赖的判据是「真子集可决定」,不是链式两步。
  • B 正确项:X→Y→Z 且 Y↛X,标准传递依赖定义。
  • C 误解来源:多值依赖记作 X↠Y,描述独立多值对应,属 4NF,不是函数依赖链。
  • D 误解来源:平凡依赖判据是 Y⊆X(如 AB→A),与链式无关。

【知识关联】

  • 主库关联:无直接对应;表设计与补-12/13 连考。
  • 国网/408:国网第 11 条;408 计算题常给出 FD 集要求「指出传递依赖/判断范式」。
  • 面试追问:① 为何传递依赖定义要求 Y↛X?→ 若 Y→X,则 X↔Y,Z 实质被 X 直接决定,无「传递」。② 传递依赖一定违反 3NF 吗?→ 当 Z 为非主属性时是;若涉及主属性则看 BCNF。③ 如何消除传递依赖?→ 分解出 Y→Z 到新表。

【拓展延伸】

  • 变式问法:①「X→Y 且存在真子集 X′→Y」→ 部分依赖;②「Y⊆X」→ 平凡依赖;③「X↠Y」→ 多值依赖。
  • 完整推导:
text
给定:学号→系号,系号→系名,且 系号↛学号(多生共享一系)
链:学号→系号→系名
因为 系号↛学号,满足传递依赖条件
故 学号→系名 是传递函数依赖
主键=学号(单属性)→ 无部分依赖 → 满足 2NF
存在传递依赖且系名非主属性 → 违反 3NF → 最高 2NF
分解:R1(学号,系号) + R2(系号,系名) → 均达 3NF
  • 工程提醒:冗余「系名」到学生表是常见反范式,换系时必须同步更新或接受短暂不一致。

补-15 ​

在 ER 模型中,若一个班级有多名学生,而一名学生只属于一个班级,则班级与学生之间的联系类型是( )。 A. 一对一(1:1) B. 多对多(m:n) C. 一对多(1:n),其中「多」在班级一侧 D. 一对多(1:n),其中「多」在学生一侧

答案:D

📌 考点定位: ER 模型与联系类型,对应国网考纲第 11、15 条,国企笔试高频。

【结论】 一个班级对应多名学生、一名学生只对应一个班级 → 班级 : 学生 = 1 : n,「多」在学生一侧。选 D。

【逐项辨析】

  • A 一对一(1:1):错。错在「一对一」——1:1 要求两端都是「一」(一个班只有一名学生),题干明确说「多名学生」。
  • B 多对多(m:n):错。错在「多对多」——m:n 要求两端都是「多」(一名学生也能属于多个班级),题干限定「只属于一个班级」。
  • C 一对多,但「多」在班级一侧:错。错在方位说反 —— 一个班级对应多名学生,「多」必然在学生一侧,不在班级一侧。
  • D 一对多(1:n),「多」在学生一侧:正确。 基数上班级为 1、学生为 n,与题干两句约束完全吻合。

【知识点】 ER 模型三要素:实体(矩形)、属性(椭圆)、联系(菱形),联系两端标注基数。

联系类型由「每一端的实例能对应另一端的几个实例」决定:

联系类型记法判定(A、B 两实体)ER 转关系模式的做法
一对一1:1A 的一个实例只对应 B 的一个,反之亦然合并为一张表,或在任一侧加外键
一对多1:nA 的一个实例对应 B 的多个;B 的一个实例只对应 A 的一个在 n 端(多端)加外键,指向 1 端主键
多对多m:n两侧都是「多」必须新建联系表,主键为两端主键的组合

本题推导:题干给了两句约束 ——「一个班级有多名学生」说明班级端指向学生端可取多个值;「一名学生只属于一个班级」说明学生端指向班级端只能取一个值。前者定「多」,后者定「一」,故为 1:n 且多在学生侧。

若把第二句改成「一名学生可选多个班级」,则两侧都是多 → m:n,必须拆出独立的选课关系表。

转换规则补充:1:n 联系转换时外键必须加在 n 端(即学生表增加「班级号」列)。不能加在 1 端 —— 否则班级表要在一个格子里存多个学生号,直接违反 1NF。这是「联系类型」与「ER 转关系模式」两个考点连考时的固定套路(见补-16)。

【记忆锚点】 「「多」的那一端拿外键」——一个班有多名学生,所以「班级号」放进学生表。

【易混对比】

  • 1:n vs m:n:看两端能否各自取多值。1:n 只需一端为多;m:n 两端都多,必须建联系表。
  • 1:1 vs 1:n:1:1 两端都是一;1:n 一端是一、一端是多。
  • 联系类型 vs 实体个数:联系类型由基数决定,与参与实体的个数(二元 / 三元)无关。
  • 换问法:若题干改成「一名学生可选多个班级、一个班级有多名学生」,答案变成 m:n;若改成「一个班只有一名班长、一名学生只能当一个班的班长」,则是 1:1。

【自测】 某系统规定「一名教师可以讲授多门课程,一门课程只能由一名教师讲授」。教师与课程的联系类型是什么?转关系模式时外键应加在哪张表?

答:1:n(「多」在课程一侧);外键「教师号」加在课程表,指向教师表主键。与补-16 连考;国网考纲第 11、15 条。

【选项误解】

  • A 误解来源:把「一个班多名学生」与「一对一」混听。1:1 要求两端都是一。
  • B 误解来源:只听到「多名」就选多对多,漏掉「学生只属于一个班」。
  • C 误解来源:一对多方向记反。「多」在学生侧不在班级侧。
  • D 正确项:班级 1 : 学生 n,多在学生侧。

【知识关联】

  • 主库关联:M30(分库分表时常按「班级/租户」维度拆,前提是 1:n 归属清晰)。
  • 国网/408:国网第 11、15 条;408 ER 转关系模式大题第一步就是判断联系类型。
  • 面试追问:① 1:n 联系外键加哪边?→ n 端(学生表加 class_id)。② m:n 为什么必须中间表?→ 否则一侧要存多值,违反 1NF。③ 1:1 何时合并成一张表?→ 两边属性都稳定访问时;否则仍拆表 + 单向外键 + UNIQUE。

【拓展延伸】

  • 变式问法:①「一师多课、一课一师」→ 1:n,多在课程侧,外键教师号进课程表;②「学生可选多课、课可被多生选」→ m:n,建 SC 表主键(sno,cno);③「一班一班长、一人只当一个班班长」→ 1:1。
  • 工程参数/命令:
sql
-- 1:n 落地:学生表持有班级外键
CREATE TABLE class (
  id BIGINT PRIMARY KEY,
  name VARCHAR(64) NOT NULL
);
CREATE TABLE student (
  id BIGINT PRIMARY KEY,
  name VARCHAR(64) NOT NULL,
  class_id BIGINT,
  FOREIGN KEY (class_id) REFERENCES class(id)
);
-- m:n 落地
CREATE TABLE sc (
  sno BIGINT, cno BIGINT, grade DECIMAL(5,2),
  PRIMARY KEY (sno,cno),
  FOREIGN KEY (sno) REFERENCES student(id),
  FOREIGN KEY (cno) REFERENCES course(id)
);
  • 设计口诀:谁是「多」,谁拿外键;多对多,中间表。

补-16 ​

将 ER 图转换为关系模式(确定表结构、主键与外键)属于数据库设计的( )阶段。 A. 逻辑结构设计 B. 概念结构设计 C. 物理结构设计 D. 需求分析

答案:A

📌 考点定位: 数据库设计阶段划分,对应国网考纲第 15 条;牛客笔试选择题数据库知识点。

【结论】 把 ER 图转换为关系模式(定表结构、主键、外键)是逻辑结构设计的标志性任务。选 A。

【逐项辨析】

  • A 逻辑结构设计:正确。 该阶段就是把概念模型(ER 图)翻译成 DBMS 支持的关系模式,并做规范化与视图设计。
  • B 概念结构设计:错。错在「概念」——概念设计的产出是 ER 图本身,与具体 DBMS 无关,还没走到「定表」这一步。
  • C 物理结构设计:错。错在「物理」——物理设计关注索引、存储结构、分区、存取路径,即「表已经定了,再决定它在磁盘上怎么放」。
  • D 需求分析:错。错在「需求」——需求分析的产出是数据流图、数据字典、需求说明书,此时还没有任何数据模型。

【知识点】 数据库设计的五个阶段(教材通用划分):

阶段核心任务产出物
需求分析调查用户需求与业务规则数据流图(DFD)、数据字典、需求说明书
概念结构设计抽象实体与联系,与 DBMS 无关ER 图
逻辑结构设计ER 图 → 关系模式;规范化;定义视图关系模式(表结构、主键、外键)、视图定义
物理结构设计选存取方法、建索引、定存储与分区索引方案、存储参数、物理文件组织
实施与运行维护建库、装载数据、调优、备份恢复运行中的数据库系统

阶段间的数据流是单向递进的:需求 → 概念模型(ER 图)→ 逻辑模型(关系模式)→ 物理实现。每一步都在「上一阶段的产出」上加工,所以判断阶段只需问一句:这一步的输入和输出分别是什么。

三个「设计」的分界口诀:

  • 概念设计:画 ER 图,只关心「有哪些实体、存在什么联系」。
  • 逻辑设计:把 ER 图翻译成表,关心主键 / 外键 / 范式。
  • 物理设计:给表配索引和存储,关心磁盘与性能。

逻辑设计阶段实际执行的操作(即本题题干所述):实体转表、属性转列、1:n 联系在 n 端加外键、m:n 联系建独立联系表(见补-15),随后做规范化检查。

【知识点扩写·逻辑设计的具体操作清单】

  1. 实体 → 表;属性 → 列(注意类型与约束)
  2. 1:1 联系 → 合并或一侧外键
  3. 1:n 联系 → n 端加外键
  4. m:n 联系 → 新建联系表,主键为两端键组合
  5. 规范化检查(至少到 3NF,必要时反范式)
  6. 定义用户视图(外模式)
  7. 与应用代码的模型映射(ORM Entity) 以上 1–6 全部属逻辑结构设计;索引与物理参数属物理设计。

【记忆锚点】 「画 ER 图 = 概念,ER 转表 = 逻辑,加索引 = 物理」。

【易混对比】

  • 概念设计 vs 逻辑设计:前者产出 ER 图(与 DBMS 无关),后者产出关系模式(依赖 DBMS)。这是本考点最常设的陷阱,分界就是「有没有变成表」。
  • 逻辑设计 vs 物理设计:前者决定「有哪些表、表之间怎么关联」,后者决定「数据在磁盘上怎么存、查询走不走索引」。
  • 需求分析 vs 概念设计:需求分析产出数据流图 / 数据字典(带业务流程视角),概念设计产出 ER 图(纯数据视角)。
  • 换问法:若题目问「确定索引和存储结构属于哪个阶段」,答案是物理结构设计;问「画出 ER 图」,答案是概念结构设计;问「数据字典属于哪个阶段」,答案是需求分析。

【自测】 「根据业务流程画出数据流图与数据字典」属于数据库设计的哪个阶段?「为订单表的 user_id 列建立 B+ 树索引」又属于哪个阶段?

答:需求分析;物理结构设计。与补-15、补-01 连考;国网考纲第 15 条。

【选项误解】

  • A 正确项:ER→关系模式(定表、主键、外键)= 逻辑结构设计的标志性任务。
  • B 误解来源:概念设计产出是 ER 图本身,还没「变成表」。
  • C 误解来源:物理设计管索引/存储/分区,此时表已定。
  • D 误解来源:需求分析产出 DFD/数据字典,还没有数据模型。

【知识关联】

  • 主库关联:补-15(ER 联系类型→逻辑设计的转换规则);M22/M23(主键选型属逻辑/物理设计交界);M21(慢查询日志——物理调优阶段)。
  • 国网/408:国网第 15 条「数据库应用系统设计与开发」;408 可能考设计阶段填空。
  • 面试追问:① 规范化属于哪个阶段?→ 逻辑结构设计。② 选 B+ 树索引属于哪阶段?→ 物理结构设计。③ 数据字典在哪阶段形成?→ 需求分析阶段开始建立,后续阶段不断充实。

【拓展延伸】

  • 变式问法:①「画 ER 图」→ 概念设计;②「ER 转表、定主键外键」→ 逻辑设计;③「建索引、选存储引擎」→ 物理设计;④「数据流图/数据字典」→ 需求分析。
  • 工程参数/命令:
text
逻辑设计产出:
  CREATE TABLE 语句 / 数据模型文档 / 外键关系 / 视图定义
物理设计动作:
  ALTER TABLE ... ADD INDEX ...
  ENGINE=InnoDB, ROW_FORMAT=DYNAMIC
  分区表 PARTITION BY RANGE ...
  选择 charset/utf8mb4, collation
运维阶段:
  慢查询日志 slow_query_log=ON, long_query_time=1
  EXPLAIN / SHOW PROFILE 调优
  • 阶段漏斗:需求 → 概念(ER)→ 逻辑(关系模式)→ 物理(索引/存储)→ 实施运维。判断阶段只需问:这一步的输入和输出是什么。

补-17 ​

事务特性中,「并发执行的事务之间互不干扰」与「事务一旦提交其修改即永久保存」分别对应( )。 A. 一致性;持久性 B. 原子性;隔离性 C. 隔离性;持久性 D. 持久性;隔离性

答案:C

📌 考点定位: 事务的 ACID 特性,对应国网考纲第 13 条,国企/银行笔试必考。

【结论】 「并发事务互不干扰」是隔离性,「提交后永久保存」是持久性。选 C。

【逐项辨析】

  • A 一致性;持久性:错。错在第一空 —— 一致性指「事务前后数据库满足完整性约束」,「互不干扰」说的是并发隔离,不是一致性。
  • B 原子性;隔离性:错。两空都不符 —— 原子性是「全做或全不做」,第二空应是「永久保存」对应的持久性。
  • C 隔离性;持久性:正确。 两空与 ACID 定义逐字对应,无需任何补充假设。
  • D 持久性;隔离性:错。错在两空颠倒 —— 「互不干扰」是隔离性,「永久保存」是持久性。

【知识点】 ACID 四特性的定义与实现机制:

特性含义(判定字眼)实现机制
原子性 Atomicity事务的操作要么全做、要么全不做Undo 日志(回滚已做的修改)
一致性 Consistency事务执行前后数据库都满足完整性约束由 A、I、D 共同保障(无独立机制)
隔离性 Isolation并发事务互不干扰,如同串行执行封锁(锁)+ MVCC
持久性 Durability提交后的修改永久生效,故障也不丢Redo 日志(重做已提交修改)

推导「为什么原子性靠 Undo、持久性靠 Redo」:

  • 事务执行到一半失败,需要擦掉已经写下的痕迹 → 用前像把数据改回原样 → Undo。
  • 事务已提交但脏页还没刷盘时宕机,需要把提交的修改补回去 → 用后像重做 → Redo。

两者共用同一份日志文件(同时记录前像与后像),只是扫描方向与用途相反(见补-20)。

一致性的特殊地位:它是目的,A、I、D 是手段。以转账为例,若隔离性被破坏(脏读)或原子性被破坏(扣钱后未加钱),一致性都会崩。因此考题里「一致性」从不与某一种日志 / 锁绑定,这正是 A 项设陷阱的地方。

隔离性的强弱由隔离级别调节:READ UNCOMMITTED → READ COMMITTED → REPEATABLE READ → SERIALIZABLE,级别越高隔离性越强、并发度越低。

【知识点扩写·ACID 与日志锁的映射】

text
Atomicity   ← Undo Log(回滚未完成事务)
Consistency ← 业务约束 + A/I/D 共同结果
Isolation   ← 锁(S/X/意向/间隙)+ MVCC(ReadView)
Durability  ← Redo Log(WAL)+ 刷盘策略
崩溃恢复:
  已提交未刷盘 → Redo
  未提交       → Undo
两阶段提交(MySQL):
  prepare(redo) → 写 binlog → commit(redo)
  任一步失败可按 binlog 是否存在决定提交或回滚

对应主库:M13 redo、M14 binlog、M15 undo、M16 两阶段提交。

【记忆锚点】 「原子靠 Undo,持久靠 Redo,隔离靠锁」——只剩一致性不绑具体机制,它由另外三个共同兜底。

【易混对比】

  • 原子性 vs 一致性:原子性是「全做或全不做」(操作层面),一致性是「数据满足完整性约束」(状态层面)。
  • 隔离性 vs 持久性:隔离性管并发(多个事务之间互不干扰),持久性管故障(宕机 / 断电后不丢)。
  • 隔离性 vs 隔离级别:隔离性是特性,隔离级别是「放宽隔离性以换取并发」的档位。
  • 换问法:若题目问「「事务中的操作要么全做要么全不做」对应哪个特性」,答案换成原子性;再问「靠什么实现」,答案换成 Undo 日志。

【自测】 某事务提交后数据库立刻断电,重启后该事务的修改依然存在。这体现了哪个特性?由什么机制保证?

答:持久性;由 Redo 日志保证(把已提交但未落盘的后像重新写入)。与补-18、补-20 连考;国网考纲第 13 条。

【选项误解】

  • A 误解来源:第一空把「互不干扰」当成一致性。一致性是事务前后满足完整性约束,是目的;互不干扰说的是并发隔离。
  • B 误解来源:两空都错——原子性是「全做或全不做」,第二空应是持久性。
  • C 正确项:互不干扰=隔离性;提交后永久=持久性。
  • D 误解来源:两空对调,最常见于记住了四个词但没绑定义。

【知识关联】

  • 主库关联:M09(默认隔离级别 RR)、M10(幻读)、M11(RR 防幻读机制)、M12(MVCC)、M13(redo log)、M15(undo log)、M16(redo+binlog 一致性)、M32(RC 能避免什么)。ACID 是这一整片题的理论总纲。
  • 国网/408:国网第 13 条「事务处理和并发控制」;408 必考 ACID 定义与实现机制对应表。
  • 面试追问:① 一致性靠什么保证?→ 不靠单一机制,由 A、I、D 以及业务约束共同保障。② 隔离性实现?→ 锁 + MVCC。③ 原子性/持久性日志?→ Undo / Redo(WAL)。④ MySQL 如何保证提交后不丢?→ redo log 二阶段提交 + binlog。

【拓展延伸】

  • 变式问法:①「全做或全不做」→ 原子性 + Undo;②「并发互不干扰」→ 隔离性 + 锁/MVCC;③「提交后宕机不丢」→ 持久性 + Redo;④「转账前后总额不变」→ 一致性(由其他特性共同保障)。
  • 工程参数/命令:
sql
-- 查看/设置隔离级别(8.0)
SELECT @@transaction_isolation;
SET SESSION transaction_isolation='READ-COMMITTED';
-- redo/binlog
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'; -- 默认1
SHOW VARIABLES LIKE 'sync_binlog';                     -- 默认1(8.0)
-- 双1配置:每次提交 redo 与 binlog 都 fsync,最安全、性能略降
  • 持久化代价:innodb_flush_log_at_trx_commit=0/2 可提升 TPS,但宕机可能丢最近 1 秒事务——金融库必须保持 1。

补-18 ​

关于两段锁协议(2PL),下列说法正确的是( )。 A. 遵循 2PL 的并发调度一定是无死锁的 B. 2PL 要求事务分为「扩展阶段(只能加锁、不能解锁)」与「收缩阶段(只能解锁、不能加锁)」,它是并发调度可串行化的充分条件 C. 2PL 要求事务在开始时一次性申请全部所需锁 D. 2PL 与事务的隔离级别无关

答案:B

📌 考点定位: 并发控制——两段锁协议,对应国网考纲第 13 条,国企笔试高频。

【结论】 2PL = 加锁全部在解锁之前完成(扩展期只加锁、收缩期只解锁),是调度可串行化的充分条件,但不保证无死锁。选 B。

【逐项辨析】

  • A 遵循 2PL 的调度一定无死锁:错。错在「一定无死锁」——2PL 只保证可串行化,与死锁无关。两事务若加锁顺序相反(T₁ 先 x 后 y、T₂ 先 y 后 x)就形成循环等待,照样死锁。
  • B 分扩展阶段与收缩阶段,是可串行化的充分条件:正确。 前半句是 2PL 的定义,后半句是 2PL 的标准结论,两项都准确。
  • C 要求开始时一次性申请全部锁:错。错在「一次性申请全部锁」——这描述的是一次封锁法(保守 2PL),是另一种并发控制策略,不是「两段锁」的本义。
  • D 2PL 与隔离级别无关:错。错在「无关」——2PL 正是实现隔离性、进而支撑各隔离级别的技术基础。

【知识点】 两段锁协议(Two-Phase Locking,2PL)的定义:

  • 扩展阶段(growing phase):只能申请锁(S 或 X),不能释放任何锁。
  • 收缩阶段(shrinking phase):只能释放锁,不能再申请任何锁。
  • 一旦释放了第一把锁,就进入收缩阶段,此后不得再加锁。

三条结论必须分开记:

命题是否成立说明
2PL ⇒ 可串行化✅充分条件
可串行化 ⇒ 2PL❌非必要条件,存在可串行化但非 2PL 的调度
2PL ⇒ 无死锁❌2PL 完全不解决死锁
2PL ⇒ 无级联回滚❌需严格 2PL(S2PL)才行

2PL 的主要变体(易考的延伸):

协议额外要求解决的问题代价
保守 2PL(一次封锁法)事务开始前一次申请全部锁可避免死锁并发度低,需预知全部锁
顺序封锁法所有事务按同一顺序申请锁可避免死锁需事先规定顺序
严格 2PL(S2PL)所有 X 锁持有到事务结束才释放避免级联回滚,保证可恢复锁持有时间长
强严格 2PL所有锁(含 S 锁)都持有到事务结束调度严格可串行化并发度进一步降低

死锁的四个处理方向:预防(一次封锁法、顺序封锁法)、检测(构造等待图,检查有无回路)、解除(选代价最小的事务回滚)。InnoDB 中另有 innodb_deadlock_detect 开关(开启时主动检测并回滚)与 innodb_lock_wait_timeout 超时(关闭检测时的兜底)两条路径。

【知识点扩写·2PL 与变体对比】

协议规则可串行化无死锁无级联回滚
基本 2PL先全加锁后全解锁✓充分✗✗
保守 2PL开始一次申请全部锁✓✓(预声明)看版本
顺序封锁按全局顺序申请✓✓—
严格 2PLX 锁持到结束✓✗✓
强严格 2PL所有锁持到结束严格可串行✗✓
InnoDB 实践:写锁持到提交 + RR 下间隙锁 → 更接近严格 2PL + 扩展封锁。

【记忆锚点】 「2PL 保可串行化,不保无死锁」——两句话必须分开记,考题十有八九就在这两句上设陷阱。

【易混对比】

  • 2PL vs 一次封锁法:2PL 允许在扩展期逐把加锁;一次封锁法要求开始时一次加完。前者不防死锁,后者可防。
  • 充分条件 vs 必要条件:2PL 是充分条件 —— 满足则必可串行化,但不满足也可能恰好可串行化。
  • 2PL vs 严格 2PL:前者不防级联回滚,后者(X 锁持到事务结束)可防。
  • 换问法:若题目问「哪种策略能避免死锁」,答案是一次封锁法 / 顺序封锁法,而不是 2PL;问「2PL 保证什么」,答案是可串行化。

【自测】 事务 T₁ 依次对 x、y 加 X 锁,事务 T₂ 依次对 y、x 加 X 锁,两者都严格遵循 2PL。它们会死锁吗?为什么?

答:会死锁。T₁ 持有 x 等 y、T₂ 持有 y 等 x,形成循环等待;2PL 只保证可串行化、不防死锁,需靠死锁检测或顺序封锁来避免。与补-19 连考;国网考纲第 13 条。

【选项误解】

  • A 误解来源:把「可串行化」与「无死锁」绑在一起。2PL 只保证可串行化,反序加锁照样死锁。
  • B 正确项:扩展期只加锁、收缩期只解锁 + 是可串行化充分条件。
  • C 误解来源:描述的是一次封锁法(保守 2PL),不是 2PL 本义。
  • D 误解来源:2PL 是实现隔离性的技术基础,与隔离级别强相关。

【知识关联】

  • 主库关联:M09–M12(隔离级别/MVCC——隔离性的实现层)、M17(无索引 UPDATE 锁范围)、M18(意向锁)、补-21(乐观/悲观锁)、补-23(InnoDB 死锁检测)。
  • 国网/408:国网第 13 条;408 并发控制大题常考 2PL 与冲突可串行化/视图可串行化的关系。
  • 面试追问:① 2PL 是可串行化的必要条件吗?→ 不是,只是充分条件,存在可串行但非 2PL 的调度。② 严格 2PL 解决什么?→ 级联回滚,X 锁持到事务结束。③ InnoDB 是严格 2PL 吗?→ 更接近:锁通常持到事务结束(尤其写锁),并配合 MVCC 快照读。

【拓展延伸】

  • 变式问法:①「2PL 保证?」→ 可串行化;②「能避免死锁的协议?」→ 一次封锁法/顺序封锁法;③「防止级联回滚?」→ 严格 2PL。
  • 完整推导·为何 2PL 可串行化:
text
设并发调度 S 中各事务均服从 2PL
冲突操作次序由锁的相容性决定
扩展期:事务持续获得锁,其冲突操作必须排在其他持锁事务之后
收缩期:只放锁,不再产生新的「排到更前」的冲突约束
可证:调度的冲突图中无环 ⇔ 可串行化
(教材结论:2PL 是可串行化的充分条件,证明见冲突可串行化定理)
反例说明 2PL⇏无死锁:
  T1: lock-x(x) … lock-x(y)
  T2: lock-x(y) … lock-x(x)
  双方均遵守 2PL,但形成循环等待
  • 工程参数/命令:
sql
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';      -- 默认50s
SHOW VARIABLES LIKE 'innodb_deadlock_detect';        -- 默认ON
SELECT * FROM information_schema.INNODB_LOCKS;        -- 5.7
SELECT * FROM performance_schema.data_locks;          -- 8.0

补-19 ​

为正确实现并发控制,关于共享锁(S 锁)与排他锁(X 锁)的申请规则与相容性,下列说法正确的是( )。 A. 读操作前申请 S 锁、写操作前申请 X 锁;S 锁之间相容,S 与 X、X 与 X 均不相容 B. 读操作前申请 X 锁、写操作前申请 S 锁 C. S 锁与 X 锁之间完全相容,可同时持有 D. 事务只需在结束时统一加锁,过程中不必申请

答案:A

📌 考点定位: 并发控制——封锁类型与相容矩阵,对应国网考纲第 13 条。

【结论】 读操作前申请 S 锁、写操作前申请 X 锁;相容矩阵中只有 S–S 相容,其余三种组合全部互斥。选 A。

【逐项辨析】

  • A 读前申请 S、写前申请 X;S–S 相容,S–X 与 X–X 均不相容:正确。 申请规则与相容矩阵两项都准确,无一处需要修正。
  • B 读操作前申请 X 锁、写操作前申请 S 锁:错。错在把两种锁的申请规则说反 —— 写操作要求独占,必须申请 X 锁,读操作共享,申请 S 锁。
  • C S 锁与 X 锁完全相容、可同时持有:错。错在「完全相容」——事实恰好相反,S 与 X 互斥,否则读事务会读到别的事务尚未提交的修改。
  • D 事务只需在结束时统一加锁、过程中不必申请:错。错在「过程中不必申请」——这等于放弃并发控制,无法保证调度的可串行性。

【知识点】 两种基本封锁类型:

锁类型别名允许的操作对其他事务的约束
共享锁 S读锁只读其他事务只能再加 S 锁,不能加 X 锁
排他锁 X写锁读 + 写其他事务既不能加 S 也不能加 X

相容矩阵(行 = 已持有,列 = 新申请;✅ 相容可立即获准,❌ 互斥需等待):

已持有 \ 新申请SX
S✅❌
X❌❌

结论:四格中只有 S–S 一格相容,其余三格全部互斥。

三条推导(对应矩阵的三格 ❌ 中的关键两格):

  • 为什么 S–S 相容?S 锁持有者只读不写,多个读者同时读互不影响,数据不会被改动,因此可以共享。
  • 为什么 S–X 必须互斥?若允许,读者可能在写者未提交时读到「脏数据」;一旦写者回滚,读者读到的值从未真实存在过(脏读)。
  • 为什么 X–X 必须互斥?两个写者同时改同一数据会导致丢失修改(后写覆盖前写),这正是 X 锁要防的核心问题。

三级封锁协议(封锁协议解决「加什么锁、持有多久」):

  • 一级:写操作加 X 锁并持到事务结束 → 防丢失修改。
  • 二级:一级 + 读操作加 S 锁、读完即放 → 再防脏读。
  • 三级:二级 + S 锁也持到事务结束 → 再防不可重复读。

补充:InnoDB 在 REPEATABLE READ 级别下还用间隙锁(Gap Lock)+ 记录锁 = Next-Key Lock 来防幻读。

【知识点扩写·三级封锁协议】

协议规则防止的问题
一级写前加 X,持到事务结束丢失修改
二级一级 + 读前加 S,读完即放脏读
三级二级 + S 也持到事务结束不可重复读
与 2PL 关系:封锁协议回答「加什么锁、持多久」;2PL 回答「加锁/解锁的时序」。两者正交,常组合使用。
InnoDB 补充:RR 下当前读使用 Next-Key Lock = 记录锁 + 间隙锁,进一步防幻读。

【记忆锚点】 「读加 S、写加 X;只有 S 和 S 能做朋友」。

【易混对比】

  • S 锁 vs X 锁:S 是「可共享的读锁」,X 是「独占的写锁」。
  • X 锁防丢失修改 vs S 锁防脏读:一级封锁协议(写加 X 持到结束)防丢失修改;二级(读加 S、读完即放)防脏读;三级(S 锁持到事务结束)防不可重复读。
  • 封锁协议 vs 两段锁协议:封锁协议解决「加什么锁、持多久」,2PL 解决「何时加、何时放」(见补-18)。两者不是一回事,常被混问。
  • 换问法:若题目问「哪两种锁相容」,答案是只有 S 与 S;问「读操作该加什么锁」,答案是 S 锁。

【自测】 事务 T₁ 已对数据项 x 加了 S 锁,此时 T₂ 申请对 x 加 S 锁、T₃ 申请对 x 加 X 锁,两者的申请分别会怎样?

答:T₂ 立即获准(S–S 相容),T₃ 需等待(S–X 互斥)。与补-18 连考;国网考纲第 13 条。

【选项误解】

  • A 正确项:读 S 写 X;相容矩阵仅 S–S 相容。
  • B 误解来源:S/X 职责说反。写操作需独占,必须 X。
  • C 误解来源:以为「都是锁所以能共存」。S 与 X 互斥,否则会脏读。
  • D 误解来源:结束时才加锁等于放弃并发控制,无法保证可串行。

【知识关联】

  • 主库关联:M17(无索引更新的锁代价)、M18(意向锁 IS/IX 在表级与行级锁的协调)、M32(RC/并发问题)、补-18(2PL 何时加放锁)、补-21(FOR UPDATE=悲观 X 锁)。
  • 国网/408:国网第 13 条;408 常考封锁相容矩阵与三级封锁协议。
  • 面试追问:① 意向锁是干什么的?→ 表级标记「内部有行锁」,快速判断表锁兼容性,避免逐行检查。② MySQL 的 LOCK IN SHARE MODE / FOR SHARE?→ S 锁;FOR UPDATE → X 锁。③ 三级封锁协议分别防什么?→ 一级防丢失修改,二级再防脏读,三级再防不可重复读。

【拓展延伸】

  • 变式问法:①「哪两种锁相容?」→ 仅 S 与 S;②「读操作加?」→ S;③「写操作加?」→ X;④「矩阵题:已持 X,申请 S?」→ 互斥等待。
  • 工程参数/命令:
sql
-- 悲观锁读/写
SELECT * FROM t WHERE id=1 LOCK IN SHARE MODE; -- S(5.7)
SELECT * FROM t WHERE id=1 FOR SHARE;           -- S(8.0)
SELECT * FROM t WHERE id=1 FOR UPDATE;          -- X
-- 查看锁
SHOW ENGINE INNODB STATUS\G  -- LATEST DETECTED DEADLOCK
SELECT * FROM performance_schema.data_locks;
  • 相容矩阵记忆:
text
        申请S   申请X
持有S    允许    等待
持有X    等待    等待

补-20 ​

数据库系统使用「日志文件 + 检查点」进行故障恢复,其核心依据是( )。 A. 故障后重建整个数据库即可,无需日志文件 B. 日志只记录已提交事务的修改,未提交事务不必记录 C. 检查点的作用是删除过期日志以节省磁盘空间 D. 利用日志对「已提交但可能未写回磁盘」的事务做 Redo、对「未提交」的事务做 Undo,检查点则用于缩短恢复时需要扫描的日志范围

答案:D

📌 考点定位: 数据库恢复技术——日志与检查点,对应国网考纲第 14 条,国企笔试高频。

【结论】 恢复依据是:对「已提交但可能未写回磁盘」的事务做 Redo、对「未提交」的事务做 Undo,检查点则用于缩短需要扫描的日志范围。选 D。

【逐项辨析】

  • A 故障后重建整个数据库即可、无需日志:错。错在「无需日志」——全库重建不现实,且会把已提交的数据一并丢掉,持久性直接失效。
  • B 日志只记录已提交事务的修改、未提交事务不必记录:错。错在「未提交不必记录」——未提交事务的修改必须记(记下前像),否则故障后无从 Undo,原子性无从保证。
  • C 检查点的作用是删除过期日志以节省磁盘空间:错。错在「删除过期日志」——检查点的作用是刷脏页并记录活跃事务表与日志位置,从而缩短恢复扫描范围;日志清理只是副产品,不是它的设计目的。
  • D 已提交做 Redo、未提交做 Undo,检查点缩短扫描范围:正确。 三句话分别对应恢复策略与检查点职责,完全准确。

【知识点】 日志文件(Log)按时间顺序记录的内容:

日志记录形式用途
事务开始<Tᵢ, START>标记事务起点
更新记录<Tᵢ, X, 前像, 后像>前像供 Undo、后像供 Redo
提交 / 回滚<Tᵢ, COMMIT> / <Tᵢ, ABORT>判定该事务该 Redo 还是 Undo

恢复策略(正向 Redo、反向 Undo):

事务状态恢复动作保证的特性
有 BEGIN 也有 COMMIT,但修改可能未落盘Redo(用后像重做)持久性
有 BEGIN 但无 COMMITUndo(用前像撤销)原子性

推导「为什么必须记录未提交事务的修改」:Undo 需要前像才能把数据改回原样。若日志里不写未提交事务的修改,宕机后就无从得知它改过什么,只能任由脏数据留在磁盘上 —— 原子性当场失效。这就是 B 项的致命错误。

检查点(Checkpoint)机制:系统定期把内存缓冲区的脏页刷回磁盘,并在日志中写入一条检查点记录,同时保存当前活跃事务表与日志位置。恢复时只需从最近一次检查点开始扫描(该点之前的修改已全部落盘),恢复时间从「扫描全量日志」降为「扫描一个区间」。

补充:检查点之后,检查点之前且已提交的事务日志可以被归档或清理,所以「节省空间」是它的副产品,而非定义 —— 这正是 C 项偷换概念的陷阱。

按故障类型选择恢复手段:

故障类型影响范围恢复手段
事务故障单个事务用日志 Undo 该事务
系统故障(宕机 / 断电)内存丢失、磁盘完好Redo 已提交 + Undo 未提交
介质故障(磁盘损坏)磁盘数据丢失备份 + 日志联合恢复

【知识点扩写·为什么必须记未提交的修改】

text
假设日志只记已提交事务:
  T 在提交前修改了 X(脏页可能已刷盘)
  宕机重启后:磁盘上可能是 T 改过的 X
  日志中却没有 T 的记录 → 无法得知前像 → 无法 Undo
  结果:未提交事务的痕迹残留在数据库中 → 原子性被破坏
因此:日志必须「先记后改」(WAL),未提交的更新也要写入,标记其事务状态
WAL 规则:数据页刷盘前,对应日志必须先落盘

对应主库 M13:redo 的 write-ahead 特性。

【记忆锚点】 「已提交 → Redo,未提交 → Undo;检查点只缩短扫描范围,不删日志」。

【易混对比】

  • Undo vs Redo:Undo 用前像擦掉未提交的改动;Redo 用后像补回已提交的改动。扫描方向:Redo 正向、Undo 反向。
  • 检查点 vs 日志清理:检查点的目的是加速恢复(缩短扫描区间),删日志只是附带效果,不是它的定义。
  • 日志 vs 备份:日志是「增量记录」(用于事务 / 系统故障恢复),备份是「全量副本」(用于介质故障,仍需配合日志重做)。
  • 换问法:若题目问「Redo 依据日志中的哪部分内容」,答案是后像;问「检查点记录保存了什么」,答案是活跃事务表与日志位置。

【自测】 系统在事务 T₃ 更新数据之后、提交之前宕机,且 T₃ 的修改可能已部分写入磁盘。重启后恢复程序应对 T₃ 执行什么操作?依据日志中的哪部分内容?

答:Undo(撤销);依据日志中的前像,把 T₃ 改过的数据恢复成更新前的值。与补-17、补-18 连考;国网考纲第 14 条。

【选项误解】

  • A 误解来源:以为可以「推倒重来」。全库重建会丢已提交数据,持久性失效。
  • B 误解来源:以为未提交不必记日志。没有前像就无法 Undo,原子性无从谈起。
  • C 误解来源:把检查点的副产品(可归档旧日志)当设计目的。检查点核心是缩短恢复扫描范围。
  • D 正确项:已提交→Redo,未提交→Undo,检查点缩短扫描区间。

【知识关联】

  • 主库关联:M13(redo log)、M14(binlog)、M15(undo log)、M16(两阶段提交保证 redo/binlog 一致)、补-17(ACID 与日志映射)。
  • 国网/408:国网第 14 条「备份和恢复」;408 恢复技术大题(日志/检查点/镜像)高频。
  • 面试追问:① 检查点记录了什么?→ 活跃事务表 + 日志位置 + 把脏页刷盘。② Redo 用前像还是后像?→ 后像;Undo 用前像。③ 崩溃恢复扫描方向?→ 从最近检查点正向 Redo 已提交,反向 Undo 未提交。

【拓展延伸】

  • 变式问法:①「Redo 依据日志中的?」→ 后像;②「Undo 依据?」→ 前像;③「检查点主要目的?」→ 缩短恢复扫描范围,不是删日志;④「介质故障怎么恢复?」→ 备份 + 日志。
  • 完整恢复流程推导:
text
系统故障(断电、宕机,磁盘完好)恢复:
  1. 从 redo log 定位最近检查点
  2. 从检查点正向扫描日志,构造:
     - REDO-LIST:检查点之后已提交事务
     - UNDO-LIST:检查点之后未提交事务
  3. 对 REDO-LIST:按后像重做(幂等:可重复执行)
  4. 对 UNDO-LIST:按前像反向撤销
  5. 写恢复完成标记
事务故障:只需 Undo 该事务
介质故障:装载最近备份 → 再用日志重做到故障前
  • 工程参数/命令:
sql
SHOW VARIABLES LIKE 'innodb_log_checkpoint%';
SHOW ENGINE INNODB STATUS\G  -- LOG 段中的 checkpoint
-- binlog 保留与清理
SHOW BINARY LOGS;
PURGE BINARY LOGS BEFORE '2026-01-01 00:00:00';

补-21 ​

关于乐观锁与悲观锁,下列说法正确的是( )。 A. 乐观锁通过数据库的行锁实现,悲观锁通过版本号实现 B. 悲观锁假定冲突频繁,先加锁再操作(如 SELECT ... FOR UPDATE);乐观锁假定冲突较少,不加锁,靠版本号或 CAS 在更新时校验 C. 乐观锁在任何场景下性能都优于悲观锁 D. 两者都必须由数据库内核提供,应用层无法实现

答案:B

📌 考点定位: MySQL 锁——乐观锁与悲观锁,互联网面试高频,主库未覆盖。

【结论】 悲观锁「先锁后改」,乐观锁「先改后校验」——前者假定冲突频繁、操作前加锁,后者假定冲突较少、不加锁而靠版本号或 CAS 在更新时校验。选 B。

【逐项辨析】

  • A 乐观锁通过数据库的行锁实现,悲观锁通过版本号实现:错。把两者的实现机制完全说反了 —— 行锁是悲观锁的手段,版本号是乐观锁的手段。
  • B 悲观锁假定冲突频繁,先加锁再操作(如 SELECT ... FOR UPDATE);乐观锁假定冲突较少,不加锁,靠版本号或 CAS 在更新时校验:正确。 一句话点全了「冲突假设 + 加锁时机 + 实现手段」三个要素。
  • C 乐观锁在任何场景下性能都优于悲观锁:错。错在「任何场景」这个绝对化字眼 —— 高冲突场景下乐观锁的重试与回滚代价很大,可能反而不如悲观锁。
  • D 两者都必须由数据库内核提供,应用层无法实现:错。乐观锁本质是应用层的「版本号 + 条件更新 + 重试」约定,不需要数据库内核提供任何锁。

【知识点】 两种锁的分野不是「谁更好」,而是「假设不同」。

悲观锁(Pessimistic Locking):假定并发冲突频繁,操作前先加锁,其他事务只能阻塞等待。由数据库内核提供:

  • 排他锁:SELECT ... FOR UPDATE → X 锁,可读可写,排斥其他任何锁。
  • 共享锁:SELECT ... LOCK IN SHARE MODE(MySQL 8.0 起写作 FOR SHARE)→ S 锁,只读,互相兼容但与 X 锁互斥。

乐观锁(Optimistic Locking):假定冲突较少,读取时不加锁,提交更新时才校验数据是否被改过。

  • 版本号法:
sql
UPDATE account SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 3;
  • 时间戳法:与版本号法同理,只是把 version 换成 update_time。
  • CAS 思想:Java 的 AtomicInteger.compareAndSet、Redis 的 WATCH + MULTI 都是「比较并交换」的乐观锁实现。
维度悲观锁乐观锁
冲突假设冲突频繁冲突较少
加锁时机操作前加锁(先锁后改)不加锁,更新时校验(先改后校验)
实现手段FOR UPDATE / LOCK IN SHARE MODE(数据库内核)版本号 / 时间戳 / CAS(应用层约定)
并发度低(阻塞等待)高(不阻塞,冲突时重试)
失败代价等待、死锁、锁超时重试(高冲突时形成重试风暴)
典型场景库存扣减、账户转账(强一致)商品详情、配置项修改(读多写少)

选型判据:冲突概率 × 重试成本 < 加锁等待成本 → 用乐观锁;反之用悲观锁。

【记忆锚点】 「悲观先锁后改,乐观先改后校验」——悲观锁像「排队过闸机」,乐观锁像「改完再对暗号,对不上就重来」。

【易混对比】

  • 乐观锁 vs 悲观锁:不是优劣之争,而是「假设之争」;冲突少用乐观、冲突多用悲观。
  • 乐观锁 vs 数据库锁:乐观锁不是数据库锁,而是应用层约定;被问「怎么实现」必须答「版本号 + 条件更新 + 重试」。
  • 乐观锁 vs MVCC:MVCC 是数据库内核提供的无锁快照读机制(解决读写冲突),乐观锁是应用层的更新写法(解决写写冲突),两者不是一回事。
  • 换问法:若题干问「SELECT ... FOR UPDATE 属于哪一类锁」,答案就是「悲观锁中的排他锁(X 锁)」。

【自测】 表 account(id, balance, version) 用乐观锁扣款。两个事务同时读到 version = 5,都要把余额减 100。第二个事务的 UPDATE 会发生什么?

答:影响行数为 0,更新失败。因为 WHERE version = 5 已不成立(第一个事务已把 version 改成 6),应用层需重新读取最新版本后重试。与补-23、主库 M17 连考;互联网面试高频。

【选项误解】

  • A 误解来源:两种锁的实现机制完全对调。行锁是悲观锁手段,版本号是乐观锁手段。
  • B 正确项:冲突假设 + 加锁时机 + 实现手段三点齐全。
  • C 误解来源:绝对化。高冲突下乐观锁重试风暴可能更慢。
  • D 误解来源:乐观锁本质是应用层约定,数据库不提供「乐观锁」这种锁类型。

【知识关联】

  • 主库关联:M17(无索引 UPDATE 的锁行为——悲观锁底层代价)、M09/M32(隔离级别影响锁行为)、补-18(2PL)、补-23(死锁——悲观锁常见副作用)、补-19(S/X 锁)。
  • 国网/408:国网第 13 条并发控制的应用延伸;面试比笔试更常考。
  • 面试追问:① 乐观锁失败应用层怎么做?→ 捕获影响行数 0,重新读版本后重试(注意幂等与退避)。② SELECT ... FOR UPDATE 与 UPDATE ... WHERE version=? 如何选?→ 冲突高用前者,读多写少用后者。③ Redis WATCH 算乐观还是悲观?→ 乐观(CAS 思想)。④ MVCC 是乐观锁吗?→ 不是,MVCC 解决读写冲突的快照读;乐观锁解决写写冲突。

【拓展延伸】

  • 变式问法:①「先加锁再操作」→ 悲观;②「更新时校验版本号」→ 乐观;③「SELECT ... FOR UPDATE 属于?」→ 悲观排他锁;④「库存扣减推荐?」→ 悲观锁或 UPDATE stock SET n=n-1 WHERE id=? AND n>=1(条件更新,兼具乐观思想)。
  • 工程参数/命令:
sql
-- 悲观
BEGIN;
SELECT balance FROM account WHERE id=1 FOR UPDATE;
UPDATE account SET balance=balance-100 WHERE id=1;
COMMIT;

-- 乐观
UPDATE account
   SET balance=balance-100, version=version+1
 WHERE id=1 AND version=5;
-- 检查 ROW_COUNT(),为 0 则重试

-- 等待与超时
SET SESSION innodb_lock_wait_timeout = 3;
  • 条件更新精简版(常被面试追问):
sql
UPDATE t SET c=c-1 WHERE id=1 AND c>0;
-- 不显式存 version,也避免了「读-改-写」竞态

补-22 ​

InnoDB 的 Buffer Pool 采用改进的 LRU 算法(把 LRU 链分为 young 与 old 两个区域),其主要目的是( )。 A. 提高磁盘 IO 的吞吐能力 B. 让所有数据页都常驻内存 C. 防止全表扫描、预读等「一次性大批量访问」把热点页挤出缓存,避免缓存污染 D. 替代 redo log 承担持久化职责

答案:C

📌 考点定位: MySQL——Buffer Pool 与 LRU 改进,互联网面试高频,主库未覆盖。

【结论】 把 LRU 链切成 young / old 两区,目的是防止全表扫描、预读这类一次性大批量访问把热点页挤出缓存,即避免缓存污染。选 C。

【逐项辨析】

  • A 提高磁盘 IO 的吞吐能力:错。Buffer Pool 是内存缓存,它存在的意义是减少磁盘 IO;改进 LRU 解决的是「该淘汰谁」的问题,不是「提高磁盘吞吐」。
  • B 让所有数据页都常驻内存:错。内存有限,不可能常驻全部数据页,innodb_buffer_pool_size 只是一个容量上限。
  • C 防止全表扫描、预读等「一次性大批量访问」把热点页挤出缓存,避免缓存污染:正确。 这正是改进 LRU 的唯一设计目的。
  • D 替代 redo log 承担持久化职责:错。Buffer Pool 是易失的内存结构,持久化由 redo log(WAL)保证。

【知识点】 Buffer Pool 是 InnoDB 在内存中缓存数据页的区域,所有读写都先经过它。若用标准 LRU,一次全表扫描会把大量数据页瞬间灌入链表头部,把链表尾部真正的热点页全部挤出去 —— 这叫缓存污染(buffer pool pollution)。

InnoDB 的改进:把 LRU 链表按比例切成两段。

区域位置默认占比存放内容
young 区(新子链)链表头部5/8真正的热点页
old 区(旧子链)链表尾部3/8新读入的页、冷数据

两个关键机制,缺一不可:

  1. 新页先插入 old 区头部(不是 young 区头部),只有被再次访问才有资格晋升。
  2. 晋升有停留时间门槛:该页在 old 区停留超过 innodb_old_blocks_time(默认 1000 毫秒)后再次被访问,才移到 young 区头部;否则(如全表扫描中同一页被快速连续访问)只在 old 区内前移位置,不晋升。

推导链条:全表扫描读入的页全部堆在 old 区 → 扫描结束后很快被淘汰 → young 区里的热点页一根汗毛都没动。所以「分区 + 时间门槛」两者必须同时存在:只分区不设门槛,扫描页会被连续访问而立即晋升;只设门槛不分区,冷页仍与热页混在同一链表里竞争。

补充:young 区头部的页被访问时也不会每次都移到最前(避免热页频繁搬移),需满足条件才移动,进一步降低链表操作开销。

【记忆锚点】 「新页先蹲冷板凳(old 区),熬够 1 秒再上位(young 区)」。

【易混对比】

  • Buffer Pool vs redo log:前者是内存缓存(易失,管读性能),后者是磁盘日志(持久,管崩溃恢复),职责完全不同。
  • 改进 LRU vs 标准 LRU:标准 LRU 只看「最近是否被访问」,改进 LRU 额外加了「分区 + 时间门槛」两道闸。
  • innodb_old_blocks_time vs innodb_buffer_pool_instances:前者控制「冷页熬多久才晋升」,后者把 Buffer Pool 拆成多个实例以减少并发争用。
  • 换问法:若题干问 innodb_old_blocks_time 的作用,答案就是「控制页在 old 区停留多久才有资格晋升 young 区」。

【自测】 把 innodb_old_blocks_time 设为 0,全表扫描造成的缓存污染会变严重还是缓解?

答:变严重。门槛为 0 意味着页进入 old 区后立刻就有资格晋升,扫描期间被访问的页会迅速涌入 young 区,热点页照样被挤出 —— 改进 LRU 的保护效果被削弱。与补-23、主库 M01 连考;互联网面试高频。

【选项误解】

  • A 误解来源:把 Buffer Pool 的「存在意义」(减少 IO)安到「改进 LRU 的目的」上。改进解决的是淘汰谁。
  • B 误解来源:不现实。内存有限,不可能全量常驻。
  • C 正确项:防全表扫描/预读造成缓存污染。
  • D 误解来源:Buffer Pool 易失,持久化靠 redo log(WAL),职责完全不同。

【知识关联】

  • 主库关联:M01(聚簇/二级索引页是 Buffer Pool 缓存对象)、M02(B+ 树与页)、M13(redo 与脏页刷盘)、M24(深分页优化——扫描压力与缓存)、补-25(ICP 减少无效回表页访问)。
  • 国网/408:408 操作系统「页面置换」可对照;国网笔试较少直接考 Buffer Pool,面试极高频。
  • 面试追问:① young/old 默认比例?→ 5/8 与 3/8(innodb_old_blocks_pct,默认 37)。② 晋升门槛参数?→ innodb_old_blocks_time 默认 1000ms。③ 如何观察命中率?→ SHOW ENGINE INNODB STATUS 中 Buffer pool hit rate;或 information_schema.INNODB_BUFFER_POOL_STATS。

【拓展延伸】

  • 变式问法:①「改进 LRU 目的?」→ 防缓存污染;②「innodb_old_blocks_time 作用?」→ old 区页停留多久后再次访问才有资格晋升;③「设为 0 会?」→ 保护失效,扫描页快速涌入 young 区。
  • 完整机制推导:
text
标准 LRU 的问题:
  全表扫描瞬间读入大量页 → 全部插到 LRU 头部
  → 业务热点页被挤到尾部淘汰 → 扫描结束后缓存里全是「一次性页」
InnoDB 改进:
  LRU = young (5/8) + old (3/8)
  新页插入 old 区头部
  仅当在 old 区停留 > innodb_old_blocks_time 且再次被访问
  → 才晋升到 young 区头部
效果:
  扫描页在 old 区快速过期淘汰
  young 区热点页不受冲击
  • 工程参数/命令:
sql
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';       -- 建议物理内存 50%~70%
SHOW VARIABLES LIKE 'innodb_old_blocks_pct';         -- 默认 37(old 占比)
SHOW VARIABLES LIKE 'innodb_old_blocks_time';        -- 默认 1000
SHOW VARIABLES LIKE 'innodb_buffer_pool_instances';  -- 多实例减锁争用
-- 动态改大小(5.7+/8.0)
SET GLOBAL innodb_buffer_pool_size = 8589934592;

补-23 ​

InnoDB 检测到死锁后,默认的处理方式是( )。 A. 主动检测(默认开启 innodb_deadlock_detect),回滚「代价最小」的那个事务,让其余事务继续执行 B. 永久挂起所有相关事务,等待人工干预 C. 直接重启数据库实例以清除死锁 D. 不做任何处理,由应用层超时后自行放弃

答案:A

📌 考点定位: MySQL——死锁检测与处理,互联网面试高频,主库未覆盖。

【结论】 InnoDB 默认开启死锁检测(innodb_deadlock_detect),发现等待成环后回滚「代价最小」的那个事务,其余事务继续执行。选 A。

【逐项辨析】

  • A 主动检测(默认开启 innodb_deadlock_detect),回滚「代价最小」的那个事务,让其余事务继续执行:正确。 「主动检测 + 牺牲一个 + 其余放行」正是 InnoDB 的默认行为。
  • B 永久挂起所有相关事务,等待人工干预:错。InnoDB 会自动打破死锁,不会永久挂起等人处理。
  • C 直接重启数据库实例以清除死锁:错。重启实例是极端运维手段,与死锁的默认处理机制无关。
  • D 不做任何处理,由应用层超时后自行放弃:错。innodb_lock_wait_timeout(默认 50 秒)只是兜底手段,不是默认的第一道处理;InnoDB 默认是主动检测。

【知识点】 死锁:两个或多个事务互相持有对方需要的锁,形成循环等待,谁也无法推进。

InnoDB 的处理流程:

  1. 构建事务等待图(wait-for graph):顶点是事务,有向边表示「事务 T₁ 正在等待 T₂ 持有的锁」。
  2. 检测环路:innodb_deadlock_detect(MySQL 5.7.15 起可显式配置,默认 ON)开启时,每当有事务需要进入锁等待,就检查加入这条等待边后是否成环。
  3. 选择牺牲者:发现环路后,挑选回滚代价最小的事务(通常按修改行数、undo log 大小衡量)回滚,释放它持有的锁,其余事务得以继续。
  4. 报错返回:被牺牲的事务收到 ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction,应用层必须重试。
  5. 超时兜底:若关闭死锁检测(innodb_deadlock_detect = OFF,可减少高并发下的检测开销),则退化为靠 innodb_lock_wait_timeout(默认 50 秒)超时释放。
机制触发条件后果是否默认
死锁检测事务等待图成环回滚牺牲者,其余事务继续✅ 默认开启
锁等待超时等待超过 innodb_lock_wait_timeout当前语句回滚并报错兜底(默认 50 秒)

预防死锁的四条实践:① 统一加锁顺序(如都按主键升序更新);② 缩短事务(快进快出,减少持锁时间);③ 给 WHERE 条件列建索引(避免行锁退化为锁全表);④ 必要时降低隔离级别(RR → RC 可减少间隙锁)。

【记忆锚点】 「死锁不等待,回滚代价最小的那个,其余照常走」——被牺牲的事务要由应用层重试。

【易混对比】

  • 死锁 vs 锁等待:锁等待是「暂时排不上队」(超时才报错),死锁是「互相卡死」(必须回滚一个)。
  • innodb_deadlock_detect vs innodb_lock_wait_timeout:前者是主动检测(默认开),后者是超时兜底(默认 50 秒)。关闭检测能省 CPU,代价是把死锁退化成慢超时。
  • 死锁 vs 脏读 / 幻读:前者是并发调度问题,后者是隔离级别问题。
  • 换问法:若题干问「死锁被回滚后应用该怎么办」,答案是「捕获 1213 错误并重试整个事务」。

【自测】 事务 A 先更新 id = 1 再更新 id = 2,事务 B 先更新 id = 2 再更新 id = 1,两者并发执行会怎样?怎么改代码避免?

答:形成死锁,InnoDB 回滚其中一个事务(报 1213),另一个继续执行;避免办法是统一按 id 升序加锁(如都先更新 id = 1 再更新 id = 2)。与补-21、主库 M17、M23 连考;互联网面试高频。

【选项误解】

  • A 正确项:默认开启检测,回滚代价最小者,其余继续。
  • B 误解来源:以为要人工运维介入。InnoDB 自动打破死锁。
  • C 误解来源:重启实例是极端手段,不是默认机制。
  • D 误解来源:把 innodb_lock_wait_timeout 兜底当成默认第一策略。默认是主动检测。

【知识关联】

  • 主库关联:M17(无索引更新导致锁升级/范围扩大→死锁温床)、M18(意向锁)、M22/M23(主键设计影响加锁顺序)、补-18(2PL 不防死锁)、补-19(锁相容)、补-21(悲观锁场景)。
  • 国网/408:408 有死锁检测(资源分配图/等待图);国网第 13 条并发控制。
  • 面试追问:① 错误码?→ ERROR 1213 (40001),应用需重试整个事务。② 高并发下为何有人关闭 deadlock detect?→ 检测本身有 CPU 开销;关闭后靠 50s 超时,延迟变差。③ 如何预防?→ 统一加锁顺序、短事务、WHERE 建索引、必要时降隔离级别。

【拓展延伸】

  • 变式问法:①「InnoDB 死锁默认?」→ 检测+回滚代价最小事务;②「等待图有环说明?」→ 死锁;③「应用收到 1213?」→ 捕获并重试;④「关闭检测的代价?」→ 死锁退化为长超时。
  • 完整流程推导:
text
T1: UPDATE ... WHERE id=1;  -- 持有 id=1 的 X
T2: UPDATE ... WHERE id=2;  -- 持有 id=2 的 X
T1: UPDATE ... WHERE id=2;  -- 等 T2
T2: UPDATE ... WHERE id=1;  -- 等 T1
等待图:T1→T2→T1 成环
InnoDB:
  选择 undo 数量/修改行数更小的事务回滚
  返回 1213
  另一事务获得锁继续
预防代码:
  两事务都先更新 id=1 再更新 id=2(统一升序)
  • 工程参数/命令:
sql
SHOW VARIABLES LIKE 'innodb_deadlock_detect';
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
SET GLOBAL innodb_deadlock_detect = OFF; -- 谨慎
SHOW ENGINE INNODB STATUS\G  -- LATEST DETECTED DEADLOCK
SELECT * FROM performance_schema.data_lock_waits; -- 8.0

补-24 ​

在 MySQL 中存储「金额」类数据,最合适的字段类型是( )。 A. FLOAT B. DOUBLE C. VARCHAR D. DECIMAL(定点数)

答案:D

📌 考点定位: MySQL——数据类型与表设计,互联网面试高频,主库未覆盖。

【结论】 金额必须用定点数 DECIMAL,二进制浮点数 FLOAT / DOUBLE 无法精确表示多数十进制小数,会导致对账不平。选 D。

【逐项辨析】

  • A FLOAT:错。4 字节单精度二进制浮点数,约 7 位有效数字,精度不可控,累加误差明显。
  • B DOUBLE:错。8 字节双精度,有效数字约 15~16 位,看似够用,但本质仍是二进制浮点,0.1 + 0.2 ≠ 0.3 这类误差照样存在。
  • C VARCHAR:错。类型错配——字符串无法做数值运算与范围排序,还会引入隐式类型转换(比较结果依赖转换规则)。
  • D DECIMAL(定点数):正确。 按「整数位 + 小数位」精确存储,DECIMAL(10,2) 表示共 10 位、其中小数 2 位,精确到分。

【知识点】 数值类型分两类:

类型存储方式精度适用场景
FLOAT / DOUBLE二进制浮点(IEEE 754)近似值,无法精确表示多数十进制小数科学计算、传感器数据
DECIMAL(M,D)定点数,按位精确存储精确值金额、汇率、税额

根本原因:十进制小数 0.1 转成二进制是无限循环小数 0.0001100110011…,只能截断存储,于是累加与比较都出现误差。例如 FLOAT 下 0.1 + 0.2 的结果约为 0.30000001192092896,在金额场景下就是「分」级别的对账差错。

DECIMAL(M,D) 的位数规则:M 为总位数(1~65),D 为小数位数(0~30),且必须 D ≤ M,默认 DECIMAL(10,0)。MySQL 5.0.3 起 DECIMAL 改为二进制格式存储(每 9 位十进制数用 4 字节),同一版本也把上限定为 M ≤ 65、D ≤ 30;比早期按字符串存储更省空间。注意版本归属是 5.0.3,不是 5.7。

【记忆锚点】 「金额用 DECIMAL,浮点只算科学」。

【易混对比】

  • DECIMAL vs FLOAT / DOUBLE:前者精确、运算略慢;后者近似、运算快。
  • DECIMAL vs 用 BIGINT 存「分」:两者都能精确,后者更省空间、运算更快,但需在业务层做单位换算 —— 都是工程常见方案。
  • DECIMAL(10,2) 的含义:总位数 10、小数位 2,即最大 99999999.99;写成 DECIMAL(2,10) 是非法的(小数位不能大于总位数)。
  • 换问法:若题干问「为什么不推荐用 DOUBLE 存金额」,答案就是「二进制浮点是近似值,累加会产生误差导致对账不平」。

【自测】 把金额字段从 DECIMAL(10,2) 改成 DOUBLE,SELECT SUM(amount) 会出现什么现象?

答:总和出现浮点误差(如 100.00 变成 99.99999999999999),无法与业务系统对账。与补-25 的表设计考点连考;互联网面试高频。

【选项误解】

  • A 误解来源:以为「小数类型」都行。FLOAT 是二进制浮点,约 7 位有效数字,金额会累加误差。
  • B 误解来源:以为 DOUBLE 精度够就万事大吉。本质仍是 IEEE754 二进制浮点,0.1+0.2≠0.3。
  • C 误解来源:类型错配。字符串不能直接数值运算/正确数值排序,还可能触发隐式转换。
  • D 正确项:DECIMAL 定点数按位精确,适合金额。

【知识关联】

  • 主库关联:M26(char/varchar 类型选择——表设计)、M08(索引前缀与类型)、补-12~14(表设计规范化后仍要选对类型)。
  • 国网/408:408 组成原理/数据库都涉及浮点表示;国网表设计相关考点。
  • 面试追问:① 为何不用 DOUBLE 存钱?→ 二进制无法精确表示多数十进制小数,对账不平。② BIGINT 存「分」可以吗?→ 可以,更省更快,业务层换算。③ DECIMAL(10,2) 什么意思?→ 共 10 位数字、小数 2 位,最大 99999999.99。

【拓展延伸】

  • 变式问法:①「金额字段选?」→ DECIMAL 或 BIGINT(分);②「科学计算/传感器?」→ DOUBLE 可以;③「DECIMAL(7,3) 范围?」→ 总 7 位小数 3 位,整数最多 4 位。
  • 完整误差推导:
text
十进制 0.1 = 二进制 0.0001100110011...(循环)
IEEE754 只能有限位截断
故 float/double 的 0.1 实际是近似值
累加 100 次 0.1:精确应为 10.00
浮点结果可能为 9.999999... 或 10.0000001
金额场景差 1 分即对账失败
DECIMAL(M,D):按十进制位精确定点存储(5.0.3 起二进制打包,每 9 位→4 字节),运算精确
  • 工程参数/命令:
sql
CREATE TABLE order_pay (
  id BIGINT PRIMARY KEY,
  amount DECIMAL(12,2) NOT NULL DEFAULT 0.00
);
-- 或
amount_cent BIGINT NOT NULL DEFAULT 0; -- 单位:分
SELECT CAST(0.1+0.2 AS DECIMAL(10,2)); -- 0.30 精确

补-25 ​

MySQL 5.6 引入的「索引下推」(Index Condition Pushdown, ICP)的作用是( )。 A. 把索引全部加载到内存,以减少磁盘 IO B. 在存储引擎层就利用索引中的列对不满足条件的记录提前过滤,从而减少回表次数 C. 自动为所有 WHERE 条件列创建索引 D. 把查询结果缓存到 Buffer Pool 中

答案:B

📌 考点定位: MySQL——索引下推,互联网面试高频,主库未覆盖;与主库 M04(覆盖索引)、M05(最左前缀)共同构成索引体系。

【结论】 索引下推(ICP)把「索引里已有的列能判断的条件」下推到存储引擎层提前过滤,只让真正满足条件的记录回表,从而减少回表次数。选 B。

【逐项辨析】

  • A 把索引全部加载到内存,以减少磁盘 IO:错。这是 Buffer Pool 的职责,与 ICP 无关。
  • B 在存储引擎层就利用索引中的列对不满足条件的记录提前过滤,从而减少回表次数:正确。 「存储引擎层过滤」与「减少回表」是 ICP 的两个关键词。
  • C 自动为所有 WHERE 条件列创建索引:错。MySQL 不会自动建索引,索引必须显式创建。
  • D 把查询结果缓存到 Buffer Pool 中:错。Buffer Pool 缓存的是数据页(不是查询结果),且 MySQL 8.0 已移除查询缓存(query cache)。

【知识点】 MySQL 架构分两层:Server 层(解析、优化、执行)与存储引擎层(InnoDB,负责读写数据页)。ICP 的本质就是把过滤动作从 Server 层下推到引擎层。

以 SELECT * FROM t WHERE name LIKE 'a%' AND age = 20、索引 idx(name, age) 为例:

无 ICP:

  1. 存储引擎用索引定位到所有 name LIKE 'a%' 的索引记录;
  2. 对每一条记录按主键回表,取出完整行;
  3. 把整行交给 Server 层,Server 层再判断 age = 20;
  4. 不满足的记录被丢弃 —— 回表白做了。

有 ICP(MySQL 5.6 引入,默认开启):

  1. 存储引擎定位到索引记录后,直接在索引上取出 age 列做判断;
  2. 只有同时满足 name LIKE 'a%' 与 age = 20 的记录才回表;
  3. 回表次数显著下降。
维度无 ICP有 ICP
过滤 age = 20 的位置Server 层存储引擎层
回表次数所有 name 命中的记录都回表仅两个条件都命中的记录回表
收益—减少回表、减少两层之间的交互

生效条件(三条同时满足):

  1. 访问类型为 range、ref、eq_ref、ref_or_null(即 EXPLAIN 的 type 列);
  2. 条件涉及联合索引中的后续列(如 (name, age) 里的 age),且该条件无法用于缩小索引扫描范围;
  3. 存储引擎支持 ICP(InnoDB、MyISAM 均支持)。

如何判断生效:EXPLAIN 的 Extra 列出现 Using index condition。 必须区分:Using index 表示覆盖索引(不需要回表);Using index condition 表示ICP(用索引下推减少了回表,但仍可能回表)。两者最常被混淆。

【知识点扩写·ICP 生效与失效】

条件生效?说明
type=range/ref/eq_ref✓前提
type=ALL 全表扫描✗无索引可用
条件列不在索引中✗引擎层读不到该列
覆盖索引已免回表无需 ICP更优路径
条件已用于缩小扫描范围可能不体现优化器已在索引层解决
注意:ICP 是 MySQL 5.6+ 的优化器/执行器协作特性,默认开启;分库分表中间件不一定下推。

【记忆锚点】 「ICP 就是把筛选提前到引擎里做,能在索引上拦下的绝不回表」——Extra 里出现 Using index condition 就是它。

【易混对比】

  • ICP vs 覆盖索引:覆盖索引是「不用回表」(索引里已含所需全部列);ICP 是「少回表」(在索引里先过滤掉一部分)。Using index ↔ 覆盖索引,Using index condition ↔ ICP。
  • ICP vs 最左前缀:最左前缀决定「能不能用上索引」,ICP 决定「用上之后能不能少回表」。
  • 下推 vs 上移:ICP 是把 Server 层的判断下推给引擎层,方向不要记反。
  • 换问法:若题干问「Extra 显示 Using index condition 说明什么」,答案就是「启用了索引下推」。

【自测】 表 t(id 主键, name, age, addr),索引 idx(name, age)。把查询改成 SELECT name, age FROM t WHERE name LIKE 'a%' AND age = 20,Extra 会显示什么?还需要回表吗?

答:显示 Using index(覆盖索引),不需要回表 —— 因为 name、age 两列都在索引里。与主库 M04、M05 连考;互联网面试高频。

【选项误解】

  • A 误解来源:把 Buffer Pool 的职责安到 ICP 上。
  • B 正确项:引擎层用索引列提前过滤,减少回表。
  • C 误解来源:MySQL 从不自动建索引。
  • D 误解来源:Buffer Pool 缓存的是页不是结果集;且 8.0 已移除 query cache。

【知识关联】

  • 主库关联:M04(覆盖索引 Using index)、M05(最左前缀)、M06(索引失效写法)、M19(EXPLAIN type)、M20(Extra 字段)、补-22(Buffer Pool)。ICP 是索引体系中「少回表」的关键一环。
  • 国网/408:国网笔试较少;互联网面试高频。
  • 面试追问:① Using index vs Using index condition?→ 前者覆盖索引不回表;后者 ICP,仍可能回表但次数更少。② ICP 生效条件?→ range/ref/eq_ref 等 + 条件涉及联合索引后续列 + 引擎支持。③ 与 MRR 关系?→ Multi-Range Read 把随机回表整理成顺序,可与 ICP 叠加收益。

【拓展延伸】

  • 变式问法:①「ICP 的作用?」→ 引擎层提前过滤,减少回表;②「Extra: Using index condition 说明?」→ 启用了索引下推;③「如何减少回表?」→ 覆盖索引、ICP、延迟关联。
  • 完整执行对比推导:
text
表 t(id PK, name, age, addr),索引 idx(name, age)
SQL: SELECT * FROM t WHERE name LIKE 'a%' AND age=20
无 ICP:
  引擎按 name LIKE 'a%' 扫 idx → 每条回表 → Server 再判 age
  回表次数 ≈ name 前缀命中行数
有 ICP:
  引擎在 idx 上直接判 age(age 已在索引里)
  仅 age=20 的记录回表
  回表次数 ≈ 同时满足两条件的行数
改写成 SELECT name, age:
  Extra=Using index(覆盖索引,0 回表)
  • 工程参数/命令:
sql
EXPLAIN SELECT * FROM t WHERE name LIKE 'a%' AND age=20;
-- Extra: Using index condition  → ICP
-- Extra: Using index            → 覆盖索引
SET optimizer_switch='index_condition_pushdown=on'; -- 默认 on
-- 延迟关联深翻页优化见主库 M24

补-26 ​

用 Redis 实现「令牌桶」限流,其核心思想是( )。 A. 每个请求都直接拒绝,不放入任何队列 B. 在固定时间窗口内计数,超过阈值即拒绝,但窗口边界处可能出现两倍流量 C. 以恒定速率向桶中放入令牌,请求取到令牌才被放行;桶的容量决定了允许的突发流量大小 D. 用 KEYS * 统计当前请求数后再做判断

答案:C

📌 考点定位: Redis——限流算法,互联网面试高频,主库未覆盖。

【结论】 令牌桶以恒定速率往桶里放令牌、桶有容量上限、请求取到令牌才放行;桶内可以积攒令牌,因此允许一定程度的突发流量。选 C。

【逐项辨析】

  • A 每个请求都直接拒绝,不放入任何队列:错。这不是限流算法,而是「全拒绝」,等同于服务不可用。
  • B 在固定时间窗口内计数,超过阈值即拒绝,但窗口边界处可能出现两倍流量:错。这句描述的本身就是固定窗口计数算法(而且把它的缺点当成了定义),不是令牌桶。
  • C 以恒定速率向桶中放入令牌,请求取到令牌才被放行;桶的容量决定了允许的突发流量大小:正确。 完整点出了「恒速放令牌 + 桶容量上限 + 消耗令牌放行」三个要素。
  • D 用 KEYS * 统计当前请求数后再做判断:错。KEYS * 是阻塞式危险命令(遍历全部 key、阻塞 Redis 主线程),线上禁用;它也根本不构成限流算法。

【知识点】 四大限流算法对比:

算法核心机制是否允许突发主要问题
固定窗口计数每个固定时间段内计数,超阈值拒绝❌窗口边界可能出现 2 倍流量
滑动窗口窗口切细并滚动,或用 ZSet 记录请求时间戳有限边界平滑,但存储与计算开销大
漏桶请求先入桶,再以恒定速率流出❌ 严格不允许流量绝对平滑,突发请求被排队或丢弃
令牌桶以恒定速率生成令牌入桶,桶有容量上限,请求消耗令牌✅ 允许突发量受桶容量约束

令牌桶的两个参数:生成速率 r(每秒放多少令牌,等于长期平均 QPS 上限)与桶容量 C(最多能攒多少令牌,等于允许的瞬时突发量)。长期平均流量被 r 限制,短时间内则可以有 C 个请求一起通过。

与漏桶的本质区别:漏桶的流出速率恒定(关注「平滑」),令牌桶的生成速率恒定(关注「限速 + 容忍突发」)。工程实现中最常用的就是令牌桶,Guava 的 RateLimiter 即其实现。

Redis 实现要点:用 INCR + EXPIRE 做固定窗口最省事;令牌桶与滑动窗口需用 Lua 脚本把「取令牌 / 更新计数」的读改写打包成原子操作,否则并发下会超发。

【记忆锚点】 「令牌桶允许突发,漏桶平滑恒定」——桶里攒的是「额度」,攒得多就能一次花掉。

【易混对比】

  • 令牌桶 vs 漏桶:前者允许突发(桶内可积攒令牌),后者不允许突发(出水速率恒定)。
  • 令牌桶 vs 固定窗口:固定窗口有边界双倍流量问题;令牌桶靠连续补充令牌,天然没有边界问题。
  • 限流 vs 熔断 vs 降级:限流是「控制进入的流量」,熔断是「下游故障时快速失败」,降级是「牺牲非核心功能」,三者常配合使用。
  • 换问法:若题干问「哪种算法流量最平滑」,答案是漏桶;问「哪种允许突发」,答案是令牌桶。

【自测】 接口限流配置为「令牌桶容量 100、生成速率 10 个/秒」。某一秒内突然涌入 100 个请求,会被拒绝吗?长期能扛住的平均 QPS 是多少?

答:前 100 个请求都能通过(桶里已攒满令牌,瞬间取完);但长期平均 QPS 上限是 10,桶耗尽后超出的请求被拒绝。与主库 R01 的 Redis 应用场景连考;互联网面试高频。

【选项误解】

  • A 误解来源:把「限流」理解成「全拒绝」,这不是算法。
  • B 误解来源:B 项描述的是固定窗口计数算法本身(还顺带写了它的边界双倍缺点),张冠李戴。
  • C 正确项:恒速放令牌 + 桶容量上限 + 取到才放行;容量决定突发上限。
  • D 误解来源:KEYS * 是危险阻塞命令,更不是限流算法。

【知识关联】

  • 主库关联:R16(不合理做法/阻塞)、R26(单线程下易阻塞操作)、R25(Pipeline 减少 RTT)、R21(Lua 保证原子)、R28(热 key)。
  • 国网/408:408 不直接考 Redis 限流;面试/网关场景高频。
  • 面试追问:① 固定窗口边界问题?→ 窗口切换瞬间可能通过 2×阈值流量。② 令牌桶与漏桶区别?→ 前者允许突发(桶内可积攒),后者出流恒定。③ 如何保证限流原子性?→ Lua 脚本打包读改写;或 Redisson RateLimiter。

【拓展延伸】

  • 变式问法:①「允许突发的算法?」→ 令牌桶;②「流量最平滑?」→ 漏桶;③「窗口边界双倍流量?」→ 固定窗口;④「ZSet 记时间戳实现?」→ 滑动窗口。
  • 工程参数/命令(令牌桶 Lua 示意):
lua
-- KEYS[1]=bucket, ARGV: capacity, rate, now, need
local tokens = tonumber(redis.call('HGET', KEYS[1], 't') or ARGV[1])
local last   = tonumber(redis.call('HGET', KEYS[1], 'ts') or ARGV[3])
local delta  = math.max(0, ARGV[3]-last) * tonumber(ARGV[2])
tokens = math.min(tonumber(ARGV[1]), tokens+delta)
if tokens >= tonumber(ARGV[4]) then
  tokens = tokens - tonumber(ARGV[4])
  redis.call('HSET', KEYS[1], 't', tokens, 'ts', ARGV[3])
  return 1
end
redis.call('HSET', KEYS[1], 't', tokens, 'ts', ARGV[3])
return 0
  • 网关对照:Nginx limit_req(漏桶/令牌桶思想)、Sentinel/Resilience4j RateLimiter、Guava RateLimiter(单机令牌桶)。
  • 固定窗口快速实现:
text
INCR  rate:{uid}:{window}
若返回 1 则 EXPIRE 秒级 TTL
若返回 > threshold 则拒绝
简单但边界不平滑

补-27 ​

下列业务场景与 Redis 数据结构的匹配,不正确的一项是( )。 A. 排行榜(按分数排序、取 Top N)——ZSet B. 购物车(存储商品的多个字段)——Hash C. 消息队列 / 最新 N 条时间线——List D. 统计独立访客(UV)并支持精确去重计数——ZSet

答案:D

📌 考点定位: Redis——数据结构与应用场景,互联网面试高频(对应主库 R01 的应用场景补充)。

【结论】 统计独立访客(UV)并精确去重计数应当用 Set(或 HyperLogLog 做近似去重),用 ZSet 属于结构错配 —— 题干问的是「不正确」的一项,故选 D。

【逐项辨析】

  • A 排行榜(按分数排序、取 Top N)——ZSet:匹配正确。ZSet 的 score 天然承载排序权重,ZREVRANGE key 0 N-1 取 Top N 为 O(log N + N)(N 为返回条数,别只记 O(log N))。
  • B 购物车(存储商品的多个字段)——Hash:匹配正确。Hash 的「field → value」正好对应「商品 id → 商品信息(数量、规格等)」。
  • C 消息队列 / 最新 N 条时间线——List:匹配正确。List 双端插入删除均为 O(1),LPUSH + LRANGE / LTRIM 天然适配队列与时间线。
  • D 统计独立访客(UV)并支持精确去重计数——ZSet:匹配错误,即本题所选的「不正确」项。 精确去重应使用 Set(SADD 天然去重、SCARD 计数);ZSet 每个成员还要额外维护一个 score,内存开销更大,而 score 对 UV 统计毫无用处。

【知识点】 Redis 五大数据类型的能力边界:

类型底层结构(小规模 → 大规模)核心能力典型场景
Stringint / embstr → raw单值、计数(INCR)缓存对象、计数器、分布式锁
Hashlistpack → hashtable字段级读写(HSET / HGET)购物车、用户信息、对象缓存
Listlistpack → quicklist双端 O(1) 插入删除、有序消息队列、最新 N 条时间线
Setintset / listpack → hashtable去重、交并差(SINTER / SUNION)UV 去重、标签、共同好友
ZSetlistpack → skiplist + dict按 score 排序、范围查询、排名排行榜、延迟队列、带权重的 Top N

「精确去重计数」的三种解法与取舍:

方案关键命令精确性内存开销
SetSADD / SCARD✅ 精确与元素数量成正比,大 UV 场景很吃内存
HyperLogLogPFADD / PFCOUNT❌ 近似(标准误差约 0.81%)固定约 12 KB,极省
BitmapSETBIT / BITCOUNT✅ 精确(要求 id 为连续整数)与最大 id 成正比

选型判据:要精确且元素量可控 → Set;只要量级、要省内存 → HyperLogLog;用户 id 是连续整数 → Bitmap。

【记忆锚点】 「排序找 ZSet,去重找 Set,对象找 Hash,队列找 List,计数找 String」——先问「我要什么能力」,再选结构。

【易混对比】

  • Set vs ZSet:Set 只有成员(去重 + 集合运算),ZSet 是「成员 + 分数」(去重 + 排序);ZSet 多出来的 score 就是额外内存开销。
  • Set vs HyperLogLog:前者精确但内存随元素增长,后者近似但固定约 12 KB。UV 上亿时基本只能选 HyperLogLog。
  • List vs ZSet 做延迟队列:List 只能顺序消费;ZSet 用时间戳作 score,可按「到期时间」范围拉取,更适合延迟任务。
  • 注意反向提问:题干问「不正确的一项」,四个选项里三个都是对的 —— 答题前先圈出问法,这是笔试高频陷阱。

【自测】 要统计「某商品页一天的独立访客数」,UV 量级在千万以上,且允许 1% 以内的误差。应选哪种 Redis 结构?

答:HyperLogLog(PFADD / PFCOUNT)。固定约 12 KB 内存、标准误差约 0.81%,满足「千万量级 + 1% 误差」的要求;若必须精确则只能退化为 Set,内存代价极高。与主库 R01、R02 连考;互联网面试高频。

【选项误解】

  • A 匹配正确:ZSet score 排序 + ZREVRANGE TopN。
  • B 匹配正确:Hash field→value 对应商品 id→属性。
  • C 匹配正确:List 双端操作 O(1),LPUSH+LTRIM 做最新 N 条。
  • D 匹配错误(本题要选的「不正确」):精确去重计数应使用 Set(SADD/SCARD)或 Bitmap;ZSet 多余 score 纯开销。题干要求「不正确」,故选 D。

【知识关联】

  • 主库关联:R01(排行榜该用什么结构)、R02(ZSet 底层 skiplist+dict)、R03(SDS)、补-28(底层编码)。
  • 国网/408:国网笔试不考 Redis 结构细节;互联网笔试/面试高频。
  • 面试追问:① UV 近千万且允许误差?→ HyperLogLog(PFADD/PFCOUNT,约 12KB,误差 ~0.81%)。② ZSet 能否去重?→ 能,member 唯一,但 UV 不需要 score。③ 共同好友用什么?→ Set 的 SINTER。

【拓展延伸】

  • 变式问法:①「排行榜 TopN」→ ZSet;②「精确去重」→ Set;③「近似去重省内存」→ HyperLogLog;④「连续整数 id 的 UV」→ Bitmap;⑤「延迟队列」→ ZSet(score=到期时间戳)。
  • 工程参数/命令:
text
# 排行榜
ZADD rank 100 user1
ZREVRANGE rank 0 9 WITHSCORES
ZREVRANK rank user1

# UV 精确
SADD uv:20260115 uid1
SCARD uv:20260115

# UV 近似
PFADD uv:hll uid1
PFCOUNT uv:hll

# Bitmap
SETBIT uv:bm uid 1
BITCOUNT uv:bm
  • 内存粗估:Set 存 1000 万整数成员可能数百 MB 级;HyperLogLog 固定 12KB。选型先问「精确性 vs 内存」。

补-28 ​

关于 Redis 的底层编码(encoding),下列说法正确的是( )。 A. 小规模数据会使用紧凑编码(如 listpack / ziplist / intset / embstr)以节省内存,达到阈值后转为 skiplist / hashtable / raw 等结构 B. Redis 各数据类型在任何规模下都固定使用同一种底层编码 C. 紧凑编码(ziplist)适合存储超大规模集合,性能优于 hashtable D. 底层编码一旦确定便永远不会转换

答案:A

📌 考点定位: Redis——底层编码机制,互联网面试高频(对应主库 R02/R03 的底层补充)。

【结论】 Redis 对小规模数据采用紧凑编码(listpack / ziplist / intset / embstr)以节省内存,规模超过阈值后自动转换为大结构(hashtable / skiplist / raw)。选 A。

【逐项辨析】

  • A 小规模数据会使用紧凑编码(如 listpack / ziplist / intset / embstr)以节省内存,达到阈值后转为 skiplist / hashtable / raw 等结构:正确。 完整点出了「紧凑编码 → 超阈值 → 大结构」这条主线。
  • B Redis 各数据类型在任何规模下都固定使用同一种底层编码:错。错在「任何规模」——编码是随规模动态变化的。
  • C 紧凑编码(ziplist)适合存储超大规模集合,性能优于 hashtable:错。说反了 —— 紧凑编码要求内存连续,插入删除要搬移数据,规模一大就退化为 O(N),超大集合应当用 hashtable / skiplist。
  • D 底层编码一旦确定便永远不会转换:错。编码会自动转换(且转换是单向的)。

【知识点】 Redis 的「数据类型」是对外接口,「底层编码」是内部实现 —— 同一类型在不同规模下用不同编码,以在「内存」与「性能」之间取舍。

数据类型小规模编码大规模编码相关配置项(默认值)
Stringint(整数值)/ embstr(≤ 44 字节)raw(> 44 字节)分界点为 44 字节
Hashlistpack(旧版 ziplist)hashtablehash-max-listpack-entries(128)、hash-max-listpack-value(64)
Listlistpack(单个 quicklist 节点)quicklist(多节点)list-max-listpack-size(128)
Setintset(全整数)/ listpackhashtableset-max-intset-entries(512)、set-max-listpack-entries(128)
ZSetlistpack(旧版 ziplist)skiplist + dictzset-max-listpack-entries(128)、zset-max-listpack-value(64)

(Redis 7.0 起,Hash / ZSet / List 中的 ziplist 已全面被 listpack 取代;listpack 最早在 Redis 5.0 引入。)

两种编码的取舍:

  • 紧凑编码(listpack / ziplist / intset):内存连续、无指针开销,省内存;但插入删除需搬移数据、查找 O(N),不适合大规模数据。
  • 大结构(hashtable / skiplist):查找 O(1) / O(log N)、插入删除友好;但每个元素带指针与哈希表开销,内存占用高。

查看编码的命令:OBJECT ENCODING key(可能返回 listpack、hashtable、skiplist、intset、embstr、raw 等)。

关键提醒:转换是单向的 —— 元素增长会转成大结构,但元素删除后不会自动转回紧凑编码(Redis 不做反向转换,以避免频繁抖动)。

【知识点扩写·为何转换是单向的】

text
扩容方向:listpack/intset → hashtable/skiplist
  触发:元素个数或元素大小超过阈值
  原因:紧凑结构插入/查找退化为 O(N),内存连续搬移代价大
缩容方向:通常不自动逆转
  原因:避免「反复跨越阈值」导致频繁 realloc 与 rehash 抖动
  工程手段:导出再导入(DUMP/RESTORE),或删除 key 后重写
面试加分点:能指出「单向转换」以及 listpack 解决 ziplist 级联更新

【记忆锚点】 「小了挤一挤(紧凑编码省内存),大了就摊开(hashtable / skiplist 保性能);转换只往前、不回头」。

【易混对比】

  • ziplist vs listpack:ziplist 每个节点存「前一节点长度」,会导致级联更新(cascade update);listpack 只存自身长度,消除了级联更新,是 ziplist 的替代者。
  • embstr vs raw:都存字符串。embstr 把 RedisObject 与 SDS 放在同一块内存(一次分配、只读,适合短字符串),raw 分两次分配(适合长字符串或需修改的场景);分界点为 44 字节。
  • intset vs hashtable:Set 元素全为整数且数量少时用 intset(有序数组 + 二分查找);一旦加入非整数元素或超过阈值,转为 hashtable。
  • 编码转换 vs 槽迁移:编码转换发生在单机内存内部(本题),与 Cluster 的槽迁移(补-29)完全是两回事。

【自测】 一个 Hash 有 200 个字段,每个字段值都很短。它的编码是什么?若删除到只剩 50 个字段,编码会变回 listpack 吗?

答:编码是 hashtable(字段数 200 超过 hash-max-listpack-entries 默认的 128);删除到 50 个字段后不会自动变回 listpack —— 编码转换是单向的。与主库 R02、R03、补-27 连考;互联网面试高频。

【选项误解】

  • A 正确项:小规模紧凑编码,超阈值转大结构。
  • B 误解来源:以为类型与编码一一固定。实际随规模动态变化。
  • C 误解来源:说反了。紧凑编码适合小规模,大集合应 hashtable/skiplist。
  • D 误解来源:编码会自动转换,且转换单向(删元素不回转)。

【知识关联】

  • 主库关联:R02(ZSet 底层 skiplist+dict)、R03(SDS 优势)、R04(Redis 为何快——编码紧凑减少内存与 CPU)、补-27(结构选型)。
  • 国网/408:408 数据结构(跳表/哈希)可对照;Redis 本身属面试向。
  • 面试追问:① OBJECT ENCODING key 看什么?→ 当前底层编码。② listpack 与 ziplist 区别?→ listpack 存自身长度,消除级联更新。③ embstr/raw 分界?→ 44 字节(RedisObject+SDS 连续分配的权衡)。

【拓展延伸】

  • 变式问法:①「Hash 小规模编码?」→ listpack;②「全整数小 Set?」→ intset;③「超过阈值后?」→ hashtable/skiplist/raw;④「删除后会自动变回紧凑吗?」→ 不会。
  • 工程参数/命令:
text
OBJECT ENCODING mykey
CONFIG GET hash-max-listpack-entries
CONFIG GET zset-max-listpack-value
CONFIG GET set-max-intset-entries
# Redis 7 多数 ziplist 已被 listpack 替代
# 自检脚本:扫描大 key 与编码
redis-cli --bigkeys
redis-cli --memkeys
  • 阈值默认(常见):
类型配置默认
Hashhash-max-listpack-entries128
Hash valuehash-max-listpack-value64
ZSetzset-max-listpack-entries128
Set intsetset-max-intset-entries512
String embstr(硬编码)44 字节

补-29 ​

在 Redis Cluster 中,节点返回的 MOVED 与 ASK 两种重定向,其区别是( )。 A. 两者完全等价,可以互换使用 B. MOVED 表示槽已永久迁移到目标节点(客户端应更新本地槽映射);ASK 表示槽正在迁移中,仅本次请求需临时转发,客户端不应更新槽映射 C. MOVED 用于读请求,ASK 用于写请求 D. ASK 表示该槽不存在,需要新建槽

答案:B

📌 考点定位: Redis——Cluster 槽迁移与重定向,互联网面试高频(对应主库 R24 的补充)。

【结论】 MOVED 是永久重定向(槽已迁走,客户端应更新本地槽映射);ASK 是一次性临时重定向(槽正在迁移中,仅本次请求转发,客户端不应更新槽映射)。选 B。

【逐项辨析】

  • A 两者完全等价,可以互换使用:错。两者语义完全不同(永久 vs 临时),客户端的处理动作也完全不同。
  • B MOVED 表示槽已永久迁移到目标节点(客户端应更新本地槽映射);ASK 表示槽正在迁移中,仅本次请求需临时转发,客户端不应更新槽映射:正确。 完整覆盖了「触发时机 + 是否更新映射」两个判据。
  • C MOVED 用于读请求,ASK 用于写请求:错。两者的区别与读写无关,只与「槽的迁移状态」有关。
  • D ASK 表示该槽不存在,需要新建槽:错。ASK 只表示「这个槽正在迁移」,槽本身是存在且有归属的。

【知识点】 Redis Cluster 把整个键空间划分为 16384 个槽(slot),槽按 CRC16(key) mod 16384 计算后分配给各节点。客户端访问某个 key 时,若该槽不归当前节点管,节点会返回重定向信息;该槽仍归本节点但正在迁移时也可能被重定向,具体分三种情形(见下表)。

情形源节点状态返回客户端动作
key 的槽不在本节点,且已稳定归属别处该槽已完全迁出MOVED <slot> <ip>:<port>更新本地槽映射,此后该槽的请求直接发往新节点
key 的槽在本节点、槽正在迁移,但该 key 已迁走槽处于 MIGRATING 状态(槽归属尚未变更)ASK <slot> <ip>:<port>先向目标节点发送 ASKING,再转发本次请求;不更新槽映射
key 的槽在本节点、槽正在迁移,但该 key 尚未迁走槽处于 MIGRATING 状态不重定向(本节点直接执行并返回结果)无需重定向处理,正常拿到回复

为什么 ASK 必须先发 ASKING?因为目标节点此时只是临时接管这个 key,它自己也认为该槽仍属于源节点。ASKING 是一次性的「放行令牌」,只对紧接着的下一条命令生效。

槽迁移的完整流程(以 redis-cli --cluster reshard 为例):

  1. 目标节点执行 CLUSTER SETSLOT <slot> IMPORTING <源节点 id>;
  2. 源节点执行 CLUSTER SETSLOT <slot> MIGRATING <目标节点 id>;
  3. 源节点对槽内每个 key 执行 MIGRATE,逐个搬走;
  4. 迁移完成后,新的槽归属在各节点间广播更新。

迁移期间源节点收到请求时:key 还在本节点 → 直接处理,不重定向;key 已迁到目标节点 → 返回 ASK(此时槽的归属还没变,客户端不能改映射);只有该槽已不再归属本节点(迁移完成、新归属已广播)才返回 MOVED。

Cluster 的扩容缩容正是通过在节点间迁移槽实现的,而不需要重算全部 key 的归属 —— 这是「槽」相对普通一致性哈希的改进点。

【记忆锚点】 「MOVED 是搬家搬完了(改地址本);ASK 是搬家搬到一半(这次先帮你转过去,地址本别动)」。

【易混对比】

  • MOVED vs ASK:判据是「槽的迁移是否完成」。槽归属已变更(迁移完成)→ MOVED + 更新映射;槽仍归本节点、只是这个 key 已迁到目标节点 → ASK + 不更新映射(key 还在源节点时由源节点直接处理,不重定向)。
  • ASK vs ASKING:ASK 是服务端返回的重定向响应;ASKING 是客户端发送的命令(一次性放行令牌)。
  • 槽 vs 一致性哈希环:槽是「固定 16384 个格子」,迁移槽即可扩容,影响面可控;一致性哈希环靠虚拟节点分摊,key 归属由哈希环决定。
  • 客户端直连 vs 代理模式:Jedis Cluster 等直连客户端需自己维护槽映射并处理重定向;Codis / Twemproxy 等代理模式对客户端透明。
  • 换问法:若题干问「客户端收到 ASK 后要不要更新槽缓存」,答案是「不要」。

【自测】 Cluster 正在把槽 3000 从节点 A 迁往节点 B(槽仍归 A 所有)。客户端向 A 请求槽 3000 中一个已迁到 B 的 key,A 会返回什么?客户端下一步怎么做?

答:A 返回 ASK 3000 <B 的地址>;客户端先向 B 发送 ASKING,再把本次请求转发给 B,且不更新本地槽映射。与主库 R24 连考;互联网面试高频。

【选项误解】

  • A 误解来源:以为都是重定向可互换。语义与客户端动作完全不同。
  • B 正确项:MOVED 永久+更新映射;ASK 临时+不更新映射。
  • C 误解来源:与读写无关,只与槽迁移状态有关。
  • D 误解来源:ASK 表示槽正在迁移,不是「槽不存在」。

【知识关联】

  • 主库关联:R24(Cluster 数据分片机制——16384 槽 CRC16)、R22(主从复制)、R23(Sentinel 与 Cluster 的定位差异)、补-28(节点内部编码,与槽迁移无关)。
  • 国网/408:408 分布式/一致性哈希可对照;Redis Cluster 属面试高频。
  • 面试追问:① 为什么是 16384 槽?→ 作者论证:心跳包大小与迁移粒度的折中(槽太多元数据大,太少迁移不均)。② 收到 ASK 下一步?→ 先发 ASKING,再发原命令,不更新槽缓存。③ 客户端模式?→ 直连(Jedis/Lettuce Cluster)自己维护槽表;代理(Codis/Twemproxy)对客户端透明。

【拓展延伸】

  • 变式问法:①「槽已迁完,节点返回?」→ MOVED,客户端更新映射;②「迁移中且 key 已迁走?」→ ASK(客户端先发 ASKING、不更新映射);③「迁移中但 key 还在源节点?」→ 源节点直接处理,不重定向;④「ASKING 作用?」→ 一次性放行令牌;⑤「槽归属计算?」→ CRC16(key) mod 16384。
  • 工程参数/命令:
text
redis-cli -c -h host -p 7000   # -c 自动处理重定向
CLUSTER INFO
CLUSTER NODES
CLUSTER KEYSLOT mykey
CLUSTER COUNTKEYSINSLOT 3000
CLUSTER GETKEYSINSLOT 3000 10
# 迁移
redis-cli --cluster reshard host:7000
# 源/目标侧
CLUSTER SETSLOT 3000 IMPORTING <src-id>
CLUSTER SETSLOT 3000 MIGRATING <dst-id>
MIGRATE dst 3000 key 0 5000
  • 完整迁移时序:
text
1) 目标:SETSLOT IMPORTING
2) 源:SETSLOT MIGRATING
3) 源逐 key MIGRATE
4) 广播最终槽归属 → 之后稳定返回 MOVED
期间:key 仍在源→源直接处理;key 已迁到目标→ASK(先发 ASKING,不改槽映射);槽归属变更后→MOVED

补-30 ​

采用「先更新数据库、再删除缓存」(Cache Aside)策略时,若删除缓存失败,最可靠的补偿手段是( )。 A. 直接忽略失败,等待缓存自然过期 B. 立即回滚数据库事务 C. 通过重试机制 + 消息队列,或订阅数据库 binlog(如 Canal)异步删除缓存,保证最终一致 D. 改为「先删除缓存、再更新数据库」并去掉补偿逻辑

答案:C

📌 考点定位: Redis——缓存与数据库一致性,互联网面试高频(对应主库 R27 的补偿手段补充)。

【结论】 删除缓存失败时,最可靠的补偿是重试机制 + 消息队列或订阅 binlog(如 Canal)异步删除缓存,目标是保证最终一致。选 C。

【逐项辨析】

  • A 直接忽略失败,等待缓存自然过期:错。会留下长时间的不一致窗口(长短取决于 TTL),期间读到的一直是脏数据。
  • B 立即回滚数据库事务:错。此时数据库更新已经提交,回滚代价极大且可能不可行(其他事务可能已基于新值操作)。
  • C 通过重试机制 + 消息队列,或订阅数据库 binlog(如 Canal)异步删除缓存,保证最终一致:正确。 两条补偿路径都指向「保证最终一致」这一目标。
  • D 改为「先删除缓存、再更新数据库」并去掉补偿逻辑:错。方向反了,反而引入新的并发脏数据问题 —— 删除缓存后、更新数据库前的读请求会把旧值回填进缓存。

【知识点】 Cache Aside(旁路缓存)是工程上最常用的缓存模式,标准流程:

  • 读:先查缓存 → 命中则返回;未命中则查库 → 把结果回填缓存(并设置 TTL)。
  • 写:先更新数据库 → 再删除缓存(注意是「删除」而不是「更新缓存」)。

为什么写时是「删除」而不是「更新」?因为更新缓存有两个问题:① 并发写时,两次「更新缓存」的执行顺序可能与「更新数据库」的顺序相反,留下脏数据;② 若缓存值需要复杂计算,写频繁时会出现「写多读少却反复重建缓存」的浪费。删除是「懒加载」,下次读到时才重建。

残余风险与补偿手段:

风险成因补偿手段
删除缓存失败网络抖动、Redis 宕机、应用重启① 重试队列 / 消息队列(至少一次删除);② 订阅 binlog(Canal / Maxwell)由数据变更驱动删除
读请求回填旧值读未命中 → 查库拿到旧值 → 写请求更新库并删缓存 → 读请求才把旧值写进缓存延迟双删、给缓存设较短 TTL、或用 binlog 驱动删除
「先删缓存再更新库」的并发问题删缓存之后、更新库之前的读请求把旧值回填不要使用这个顺序(这正是选项 D 的错误方向)

两种补偿方案对比:

方案侵入性可靠性说明
重试 + MQ需业务代码配合(发消息、消费重试)较高(要求消息不丢、消费幂等)实现简单,适合中小规模
订阅 binlog(Canal)对业务代码无侵入更高(以数据库提交为准,天然有序)需额外部署与运维 Canal 等中间件

理论边界:强一致做不到 —— 缓存与数据库是两个独立系统,无法用同一个事务包住;工程目标是最终一致,并把不一致窗口压到最短。

【记忆锚点】 「先库后缓存、删而不改、失败补偿(重试 / MQ / binlog)」——三件套缺一不可。

【易混对比】

  • 删除缓存 vs 更新缓存:Cache Aside 选删除(懒加载、避免并发写乱序);只有「读极多、写极少」且能容忍短暂脏数据时才考虑更新缓存。
  • 先删缓存再更新库 vs 先更新库再删缓存:前者在并发下会把旧值回填,风险更大;标准做法是后者。
  • 缓存一致性 vs Cluster 重定向:MOVED / ASK(补-29)是 Cluster 内部的请求转发,本题讲的是「库 - 缓存」之间的数据一致性,两者无关,是常见混淆点。
  • 重试 + MQ vs binlog 订阅:前者有侵入、依赖消息可靠性;后者无侵入、以数据库为准,可靠性更高但需部署中间件。
  • 换问法:若题干问「为什么要删缓存而不是更新缓存」,答案就是「避免并发写导致缓存与库顺序错乱,并避免写多读少时反复重建缓存」。

【自测】 用 Canal 订阅 MySQL binlog 来删除 Redis 缓存,相比「在业务代码里删缓存 + 失败进 MQ 重试」,主要优势是什么?

答:对业务代码无侵入,且以数据库提交的 binlog 为准(顺序可靠、不依赖业务侧是否漏发消息),一致性更强;代价是需要额外部署与运维 Canal 组件。与主库 R27、补-29 连考;互联网面试高频。

【选项误解】

  • A 误解来源:「等 TTL 过期」窗口过长,业务读到脏数据。
  • B 误解来源:DB 更新往往已提交,此时回滚代价极大甚至不可行(别的事务可能已读到新值)。
  • C 正确项:重试+MQ 或订阅 binlog(Canal)异步删,保证最终一致。
  • D 误解来源:方向反了。「先删缓存再更新库」在并发下容易把旧值回填进缓存。

【知识关联】

  • 主库关联:R27(缓存与数据库一致性策略)、R05–R10(穿透/击穿/雪崩——不一致会放大故障面)、M14(binlog——Canal 的数据源)、M16(binlog 与 redo 一致)、补-29(Cluster 场景下删缓存要删对节点)。
  • 国网/408:408 事务/日志可对照;互联网工程面试必考。
  • 面试追问:① 为什么是删缓存不是更新缓存?→ 并发下更新顺序可能与 DB 相反;复杂值重建浪费。② 什么是延迟双删?→ 更新 DB 后删缓存,延迟几百毫秒再删一次,覆盖「回填旧值」竞态。③ Canal 原理?→ 伪装 MySQL slave 拉取 binlog,解析后驱动缓存删除/消息。

【拓展延伸】

  • 变式问法:①「删除缓存失败最可靠补偿?」→ 重试/MQ/binlog 订阅;②「先库后缓存还是先缓存后库?」→ 标准 Cache Aside 是先库后删缓存;③「强一致可能吗?」→ 缓存与 DB 两系统难同一事务,目标最终一致。
  • 完整竞态推导:
text
场景:读回填旧值
  t1 读缓存未命中
  t2 读 DB 得到旧值 V0
  t3 写事务更新 DB 为 V1,并删缓存
  t4 步骤 t2 的读请求才把 V0 写入缓存
  → 缓存长期停留在 V0(不一致)
补偿:
  较短 TTL + 延迟双删 + binlog 驱动删除
Canal 路径:
  DB commit → binlog → Canal → MQ/消费 → DEL cache
  以 DB 为准,业务代码无侵入
  • 工程参数/命令:
text
# Canal 拉取 binlog 需要
log_bin=ON
binlog_format=ROW
binlog_row_image=FULL
# 应用侧
CacheAside: get → miss → load db → setex key ttl value
write: update db → del key(失败进重试队列)
# 延迟双删:del 后 sleep(500ms) 再 del
# Redis
SET key value EX 300
DEL key
  • 方案对比:
方案侵入可靠性复杂度
忽略+TTL无低最低
同步删失败回滚高差(DB 已提交)高
MQ 重试中较高中
Canal binlog低高需运维组件

持续学习,持续积累。