CMU 15-445 数据库系统导论(Fall 2024)课程笔记
一、课程安排与外部活动
1. 作业与项目进度
- 已截止任务:作业一(Assignment 1)和项目零(Project 0)于课程前一晚到期,项目零整体完成质量较好。
- 项目一(Project 1)发布:计划当天或次日发布,需同步上传源代码、教程、排行榜及完整文档;项目基于 “内存管理” 相关讲座(需实现自定义缓冲区),可能在周三简要讨论,或推迟至下周。
2. 外部讲座与技术活动
| 活动主题 | 时间 | 地点 / 福利 | 备注 |
|---|---|---|---|
| Databricks 技术讲座 | 次日 18:00 | 盖茨大楼 | 内容围绕 Mosaic LLM,提供食物 |
| Snowflake 技术讲座 | 本周四 12:00 | 9 层 | 提供比萨,主讲人含课程往届学生 |
| Influx 数据融合研讨会 | 第 23 天(周一) | - | 属于课程研讨会系列,主题为 “构建块” |
| MDavis 集团校园活动 | 下周周一、周二 | 校园内 | 面向本课程学生开放,含研究讲座、海报展 |
3. 企业招聘与交流
- 信息会议:下周周二,各赞助公司(除 Confluent 外)举办 1 小时信息会,介绍业务、实习及全职岗位。
- 简历投递:需将简历提交至 Piazza 发布的电子表格,课程团队将同步给所有企业。
- 预约交流:通过 Piazza 报名表选择与企业的会面时间。
二、数据库存储架构核心
1. 磁盘导向数据库:面向元组的架构(Tuple-Oriented Architecture)
(1)核心设计假设
- 数据主要存储于非易失性存储设备(磁盘),系统需处理 “数据不在内存、需从磁盘加载” 的场景,且无法在磁盘上原地更新元组(需加载至内存修改后写回)。
- 核心是面向元组的组织方式:数据库管理系统(DBMS)需跟踪元组的位置(“元组在哪一页、页内有哪些元组”),基于 “堆文件(Heap File)” 存储无序元组。
(2)插槽页(Slotted Page)方案
是面向元组架构的核心页面组织方式,通过 “间接层” 实现元组灵活管理,结构如下:
- 页面标题(Header):存储页面元数据(如校验和、空闲空间大小、插槽数组长度)。
- 插槽数组(Slot Array):从页面起始位置向末尾增长,每个插槽存储元组在页内的偏移量(支持固定 / 可变长度元组)。
- 元组数据区:从页面末尾向起始位置增长,与插槽数组 “相向而行”,页面满时两者相遇。
- 核心优势:间接引用(通过插槽号而非直接偏移量定位元组)。若需重新组织页内元组(如删除元组后回收空间),仅需更新插槽数组的偏移量,无需修改外部引用(如索引),降低维护成本。
- 代价:访问元组需多一步 “插槽数组查找”,但相比磁盘 IO 可忽略。
(3)记录 ID(Record ID / RID):元组的物理地址
- 定义:代表元组在数据库中的物理位置,是 DBMS 内部用于定位元组的 “坐标”,而非应用层的逻辑键(如主键)。
- 组成:通常为 “文件 ID + 页面 ID + 插槽号”(不同 DBMS 略有差异),示例如下:
| 数据库系统 | Record ID 形式 | 特点 | 删除 / 插入处理逻辑 |
|---|---|---|---|
| Postgres | (页面 ID, 插槽号) | 插槽号从 1 开始;删除元组后留空槽,需通过VACUUM FULL(垃圾回收)重新利用空间 | 插入新元组默认追加至末尾,VACUUM后紧凑页面 |
| SQLite | 单调递增的行 ID(Row ID) | 作为默认合成主键,基于 B + 树索引定位元组 | 删除后不重用 ID,无 “真空” 机制,无法紧凑页面 |
| SQL Server | 十六进制物理地址(含文件 ID、页 ID、插槽号) | 删除元组后自动紧凑页面,重用空闲插槽 | 插入新元组优先填充空闲槽,无槽时追加至末尾 |
| Oracle | 字节数组(含对象 ID、文件号、块号、插槽号) | 插槽号从 0 开始;删除后不紧凑页面 | 插入新元组默认追加至末尾 |
- 注意:应用层不应依赖 Record ID,因关系模型中表数据无序,元组可能被移动(如 Postgres 的
VACUUM、SQL Server 的紧凑),导致 Record ID 变化。
(4)面向元组架构的问题
- 碎片化:删除元组后若不紧凑(如 Postgres 默认行为),页内会产生空闲槽,导致页面利用率低。
- 磁盘 IO 浪费:访问单个元组需加载整个页面(磁盘 IO 粒度为页面),更新 10 个分散在不同页的元组需读取 10 个页面,存在大量无用数据加载。
- 不兼容非原地更新设备:SSD 需覆盖整个区块、分布式文件系统(如 HDFS、S3)仅支持追加写,无法支持 “加载 - 修改 - 写回” 的原地更新逻辑。
2. 日志结构存储(Log-Structured Storage / LSM Tree)
为解决面向元组架构的缺陷(IO 浪费、不支持追加写设备)而设计,核心是 “用顺序追加写替代随机更新”,适用于写入密集型场景。
(1)核心思想
- 不直接更新元组,而是将所有修改(插入、删除、更新)记录为日志条目,通过 “内存表暂存 + 磁盘表持久化” 的分层结构优化读写性能。
- 关键假设:内存中可快速进行原地更新,磁盘仅支持顺序追加写;通过 “后台压缩” 合并冗余日志,保证读取效率。
(2)核心组件与流程
- 内存表(MemTable):
- 驻留内存的有序数据结构(如跳表、B + 树),用于接收最新的写操作(插入、删除、更新),支持快速键值查找和原地更新。
- 当 MemTable 达到容量阈值(如几百 MB),会 “冻结” 并异步写入磁盘,转为SSTable(Sorted String Table),同时创建新 MemTable 接收新写操作。
- SSTable(Sorted String Table):
- 磁盘上的有序 immutable 文件(仅追加、不修改),内部按键(Key)排序,存储 “键 - 值(元组)” 或 “键 - 删除标记(墓碑)”。
- 按 “层级(Level)” 组织:新生成的 SSTable 放入 Level 0,随着 Level 内 SSTable 数量达到阈值,触发 “压缩(Compaction)” 合并到更高 Level(Level 1、Level 2…),Level 越高,SSTable 越大、数量越少。
- 压缩(Compaction):
- 目的:合并同一 Level 或相邻 Level 的 SSTable,删除冗余条目(如旧版本元组、已删除元组的墓碑),减少磁盘占用并优化读取。
- 过程:基于 “排序合并算法”—— 因 SSTable 已按键排序,通过迭代器遍历多个 SSTable,比较键值,保留最新版本的条目(时间戳或 Level 先后判断新旧),生成新 SSTable 写入更高 Level。
- 读取流程:
- 优先查 MemTable(最新数据);若未命中,按 Level 从低到高(Level 0 → Level 1 → …)查 SSTable。
- 优化手段:
- 总结表(Summary Table):存储每个 SSTable 的 “最小键 - 最大键”,快速判断键是否在该 SSTable 范围内。
- 布隆过滤器(Bloom Filter):为每个 Level 或 SSTable 构建布隆过滤器,快速排除 “键不存在” 的情况,避免不必要的磁盘 IO。
(3)优缺点与实际应用
| 优点 | 缺点 | 典型应用 |
|---|---|---|
| 写入快(顺序追加,无随机 IO) | 读取慢(需遍历多 Level SSTable) | RocksDB(Facebook,基于 LevelDB) |
| 兼容仅追加写设备(HDFS、S3) | 写放大(元组生命周期中被多次重写) | Cockroach DB、Neon(改造 Postgres 存储层) |
| 后台压缩不阻塞前台写操作 | 压缩开销大(CPU/IO 密集) | 分布式数据库、键值存储 |
3. 索引组织存储(Index-Organized Storage)
与 “堆文件 + 独立索引” 的模式不同,索引本身即存储—— 索引的叶子节点直接存储元组,无需通过 Record ID 定位元组,适用于读取密集型场景。
(1)核心设计
- 基于有序索引(如 B + 树)组织元组:内部节点为 “路标”(存储键范围,指引查找方向),叶子节点按键排序并直接存储完整元组(而非 Record ID)。
- 页面结构:叶子节点页面类似 “有序插槽页”—— 插槽数组按键排序,元组数据区也按键顺序存储,支持二分查找快速定位元组。
(2)查找流程
- 基于查询键遍历索引(如 B + 树),定位到对应的叶子节点页面。
- 在叶子节点的插槽数组中二分查找目标键,直接通过偏移量获取元组(无需额外查找堆文件)。
(3)适用场景与数据库
- 适合 “按主键频繁查找” 的场景(如用户表按用户 ID 查询),避免 “索引查找→Record ID→堆文件” 的两步操作。
- 支持数据库:SQLite(默认 Row ID 索引组织)、MySQL(InnoDB 引擎默认聚簇索引)、Oracle(可选 “索引组织表” 模式)、SQL Server(支持聚簇索引)。
三、元组的内部结构
元组本质是有序字节序列,DBMS 通过 “结构定义” 赋予字节意义,核心组成如下:
- 元组头部(Tuple Header):
- 存储元组元数据,如:
- 元组长度、属性数量;
- 空值标记(如位图,标记哪些属性为 NULL);
- 事务相关信息(如版本号,用于多版本并发控制)。
- 头部大小固定(不同 DBMS 略有差异),确保属性数据区偏移量可计算。
- 存储元组元数据,如:
- 属性数据区:
- 按表定义的属性顺序存储字节,如 “32 位整数 ID + 64 位整数 Value” 的元组,数据区为 “4 字节 ID + 8 字节 Value”。
- 关键问题:数据对齐—— 现代 CPU(64 位)要求数据在 “字长边界”(如 8 字节)对齐,否则可能触发性能损耗或错误。若表中混合短类型(如 1 字节布尔值)和长类型(如 8 字节浮点数),需通过 “填充字节” 保证对齐。
四、关键概念总结
| 概念 | 核心定义 | 核心作用 |
|---|---|---|
| 插槽页(Slotted Page) | 面向元组架构的页面组织方式,含标题、插槽数组、元组数据区 | 灵活管理页内元组,减少外部引用维护 |
| Record ID | 元组的物理地址(文件 ID + 页 ID + 插槽号) | DBMS 内部定位元组的核心坐标 |
| LSM Tree | 日志结构存储的实现,含 MemTable、SSTable、Compaction | 用顺序写优化写入密集场景 |
| 索引组织存储 | 索引叶子节点直接存储元组,索引即存储 | 优化读取密集场景,减少查找步骤 |
| 数据对齐 | 元组属性数据按 CPU |
问答
以下 10 道问答题围绕课程笔记核心知识点设计,涵盖数据库存储架构、关键组件原理、不同系统特性等,答案严格对应笔记内容,突出核心逻辑与细节:
1. 插槽页(Slotted Page)方案中,“间接引用” 通过什么实现?这种设计的核心优势是什么?
答案:
- 间接引用通过 “插槽数组” 实现:插槽数组存储元组在页内的偏移量,访问元组时需先查插槽数组获取偏移量,再定位元组数据。
- 核心优势:重新组织页内元组(如删除后回收空间)时,仅需更新插槽数组的偏移量,无需修改外部引用(如索引),降低维护成本;支持固定长度和可变长度元组的灵活管理。
2. 不同数据库系统的 Record ID(记录 ID)形式存在差异,请对比 Postgres 和 SQL Server 在删除元组后,Record ID 对应的插槽处理逻辑有何不同?
答案:
- Postgres:Record ID 为 “页面 ID + 插槽号”(插槽号从 1 开始),删除元组后会保留空插槽,不自动紧凑页面;需手动执行
VACUUM FULL(垃圾回收)才能重新利用空插槽,插入新元组默认追加至页面末尾。 - SQL Server:Record ID 为含文件 ID、页 ID、插槽号的十六进制地址,删除元组后会自动紧凑页面(将后续元组前移),重用空闲插槽;插入新元组时优先填充空闲槽,无空闲槽则追加至末尾。
3. 日志结构存储(LSM Tree)中的 MemTable 和 SSTable 分别是什么?MemTable 何时会转换为 SSTable?
答案:
- MemTable:驻留内存的有序数据结构(如跳表、B + 树),用于接收最新写操作(插入、删除、更新),支持快速键值查找和原地更新,容量通常为几百 MB。
- SSTable:MemTable 达到容量阈值后 “冻结” 并异步写入磁盘的 immutable 文件,内部按键排序,存储 “键 - 元组” 或 “键 - 删除墓碑”,按 “层级(Level)” 组织。
- 转换条件:当 MemTable 的容量达到预设阈值(如填满内存分配的空间),会冻结当前 MemTable 并生成新 MemTable 接收新写操作,冻结后的 MemTable 异步写入磁盘成为 Level 0 的 SSTable。
4. LSM Tree 中的 “压缩(Compaction)” 操作是什么?触发压缩的条件和核心目的是什么?
答案:
- 压缩操作:将同一 Level 或相邻 Level 的多个 SSTable(均按键排序)通过 “排序合并算法” 合并为新 SSTable,保留键的最新版本(按时间戳或 Level 先后判断),删除冗余条目(旧版本元组、删除墓碑)。
- 触发条件:当某 Level 的 SSTable 数量达到阈值(如 Level 0 最多存 4 个 SSTable),触发该 Level 与更高 Level 的部分 SSTable 合并,合并后的新 SSTable 存入更高 Level。
- 核心目的:① 减少磁盘占用(删除冗余数据);② 优化读取性能(减少需遍历的 SSTable 数量);③ 维持 LSM 的层级结构,避免低 Level SSTable 过多导致读取效率下降。
5. 面向元组的架构(Tuple-Oriented Architecture)和日志结构存储(LSM Tree)分别适用于什么场景?请说明原因。
答案:
- 面向元组的架构:适用于读取密集、更新分散的场景。
原因:基于堆文件和插槽页,元组一旦写入磁盘(忽略碎片化处理),生命周期内无需重复写入,无 “写放大” 问题;但更新需加载整个页面,随机 IO 较多,写入效率低。 - 日志结构存储:适用于写入密集、读取可接受一定延迟的场景(如分布式数据库、键值存储)。
原因:通过 MemTable 暂存写操作、SSTable 顺序追加写,避免随机 IO,写入速度快;但读取需遍历多 Level SSTable(依赖布隆过滤器和总结表优化),且压缩会导致 “写放大”(元组多次重写)。
6. 索引组织存储(Index-Organized Storage)与 “堆文件 + 独立索引” 的模式相比,核心区别是什么?这种区别带来了什么优势?
答案:
- 核心区别:索引组织存储中,索引的叶子节点直接存储完整元组;而 “堆文件 + 独立索引” 中,索引叶子节点仅存储元组的 Record ID,需通过 Record ID 到堆文件中查找元组。
- 优势:减少查找步骤(无需 “索引→Record ID→堆文件” 的二次定位),显著提升 “按索引键查找元组” 的效率,尤其适合按主键频繁查询的场景(如用户表按用户 ID 查询)。
7. Postgres 中,删除元组后为何会出现 “页面碎片化”?如何解决这一问题?
答案:
- 碎片化原因:Postgres 的插槽页默认不自动紧凑页面 —— 删除元组后,仅标记对应插槽为 “空闲”,不移动其他元组填充空闲空间,导致页内出现 “空槽”,页面利用率降低(即碎片化)。
- 解决方法:执行
VACUUM FULL命令(Postgres 的垃圾回收机制):① 扫描表的所有页面,收集可见元组(排除已删除、过期版本的元组);② 创建新页面存储这些可见元组,按顺序排列(无空槽);③ 释放原页面的碎片化空间,实现页面紧凑。
8. 在 LSM Tree 的读取流程中,如何避免遍历所有 Level 的 SSTable?依赖哪些优化组件?
答案:
- 避免全遍历的核心逻辑:按 “从新到旧” 的顺序查找(先查 MemTable,再查 Level 0→Level 1→…),找到键的最新版本后立即返回,无需继续遍历更高 Level。
- 关键优化组件:
① 布隆过滤器(Bloom Filter):为每个 Level 或 SSTable 构建,快速判断 “键是否可能存在于该 SSTable”,若判断 “不存在” 则直接跳过该 SSTable/Level,避免无效磁盘 IO;
② 总结表(Summary Table):存储每个 SSTable 的 “最小键 - 最大键”,快速判断键是否在该 SSTable 的键范围内,若不在则跳过。
9. 元组内部结构中的 “数据对齐” 是什么?为何数据库系统需要关注数据对齐?
答案:
- 数据对齐:指元组的属性数据按 CPU 的 “字长边界”(如 64 位 CPU 的 8 字节边界)存储,即属性数据的起始地址是字长的整数倍。
- 关注原因:现代 CPU 仅高效支持对齐数据的访问 —— 若数据未对齐,CPU 需执行额外操作(如分两次读取再拼接),导致性能损耗;严重时(如某些硬件架构)可能直接触发访问错误,因此 DBMS 需通过 “填充字节” 调整属性位置,确保数据对齐。
10. SQLite 的 Row ID 和 Oracle 的 Row ID 在形式和特性上有何差异?
答案:
| 对比维度 | SQLite 的 Row ID | Oracle 的 Row ID |
|---|---|---|
| 形式 | 单调递增的整数(如 1、2、3…) | 字节数组(含对象 ID、文件号、块号、插槽号) |
| 核心特性 | 作为默认合成主键,基于 B + 树索引定位元组;删除元组后不重用 ID,无 “真空” 机制 | 代表元组的物理位置,插槽号从 0 开始;删除元组后不紧凑页面,新元组默认追加 |
| 应用层依赖风险 | 不建议依赖(元组删除后 ID 不重用,可能出现间隙) | 不建议依赖(元组移动后 Row ID 可能变化) |
1629

被折叠的 条评论
为什么被折叠?



