CMU 15-445前三讲笔记:关系模型、SQL与存储页布局核心解析
花了几个周末把CMU 15-445cmu15445的前三讲啃完了趁着记忆还热乎赶紧整理成笔记。这门课在数据库圈子里什么分量不用我多说Andy Pavlo亲自带队所有课件、作业、考试都公开号称“数据库系统领域的CSAPP”。我这次整理的是lec1到lec3关系模型、SQL语言、存储层设计刚好是整门课的地基三件套。无论你是准备秋招面试补数据库底层知识还是工作中被慢SQL虐过想搞清楚DBMS内部到底在干嘛前几讲都值得反复咀嚼。这篇笔记不是复读幻灯片我尽量把Andy课上反复强调的核心逻辑、我踩过的理解误区、以及能直接映射到日常工作的点都写清楚。说实话第一次看lec1-3的时候我犯了一个错误以为就是讲SQL和建表随便扫一眼就过去了。真正静下心跟完才发现这三节课的信息密度极高尤其是存储层那部分直接决定了后面缓冲区管理、索引结构、并发控制能不能听懂。所以这篇笔记我按“关系模型 → SQL → 存储布局”的顺序拆开讲每一步都尽量解释清楚背后的为什么而不是只记结论。1. 内容整体设计与思路拆解1.1 为什么cmu15445前三讲是整门课的承重墙数据库系统这门课看起来知识点很散实际上有一条清晰的主线数据怎么存、怎么查、怎么并发、怎么恢复。lec1-3对应的就是“数据怎么存”和“怎么查”的入门形态后面的所有高级话题几乎都在前三讲的假设之上做文章。比如lec3讲的页page和记录record布局到后面讲索引时你会反复遇到这些概念B树的叶子节点本质上就是某种形式的页节点分裂时要移动记录如果对slotted page不熟悉看索引实现会像看天书。同理没有理解关系代数是SQL的“编译前形态”就无法真正明白为什么SQL声明式写法能表达各种复杂查询也更难理解查询优化器在做什么——它做的事其实就是把你写的SQL翻译成关系代数执行树再想办法换一种成本更低的等价代数表达式。我的建议是前三讲宁可放慢速度也一定要做到“合上视频能自己画出页布局图、能写出关系代数表达式、能默写出SQL执行顺序”。这一遍的深度决定了你后面是听得懂还是听得懵。1.2 课程节奏与lec1-3的内容边界2023秋季这门课的实际录制版本里lec1是课程导论加关系模型lec2是SQL和关系代数lec3是存储结构。按Andy自己的说法他会在前几讲快速把关系模型交代清楚然后立刻进入SQL实操因为SQL是后面的所有作业比如Project 1的存储、Project 2的哈希索引要用的基础工具。需要注意这里的关系模型和“关系型数据库”里的“关系”是一个意思但和“关系代数”不同。关系模型描述的是数据结构本身——一张表、一个元组、一个属性关系代数描述的是怎么操作这些结构。lec1讲前者lec2讲后者。这个区分如果你没留意后面看你可能会把select操作和SQL的SELECT关键字搞混。还需要做好一个心理准备这门课不是让你学会用某个数据库工具的而是让你能自己写出一套数据库。所以前几讲里Andy会反复强调“DBMS不知道你的表长什么样它只看到字节”这句话是理解存储设计的钥匙。1.3 我读这一遍时的目标拆解我给这次学习定了三个可验证的产出目标对每张PPT里的关键图比如主键外键约束图、页布局示意图能不看笔记重新画出来并讲清每部分的含义。把lec2里所有SQL示例在SQLite和PostgreSQL里各跑一遍尤其要试出那些“看起来很对但一执行就报错”的写法加深记忆。在本地Python里实现一个简化版的slotted page内存结构验证变长记录插入和删除时槽位怎么移动。这样的目标比“看完视频”更有意义。看完视频只是输入能写出来、跑出来、画出来才是真正消化。2. 第1讲核心关系模型不是“建表”那么简单2.1 关系模型的三大组件Andy在lec1给的框架是关系模型由结构structure、完整性integrity、操纵manipulation三部分组成。结构就是表、列、行完整性就是主键、外键、唯一约束这些规则操纵就是关系代数和SQL能做的那些查询变换。我第一遍看的时候觉得这太抽象了后来找到一个类比你可以把关系模型想象成Excel的“强规则版”。Excel允许你在同一列里塞日期、数字和文字也允许两行完全相同但关系模型通过域domain和键约束把这些漏洞堵死了。域就是每列必须是某一种类型主键就是每一行必须有唯一标识外键就是列与列之间的引用关系必须合法。这个理解到位后再看为什么关系模型当年能打败层次模型和网络模型就顺了层次模型像是一棵严格的树网络模型像是复杂的网状指针你想查一个数据必须知道路径怎么走而关系模型把全部数据摊成二维表用一套统一的代数语言SQL查询用户不用关心底层存储是怎么组织的。这就是“物理数据独立性”的核心价值——底层存储随便换你写的查询不用改。实操笔记建议自己动手在纸上把“学生表、课程表、选课表”画出来标出每个表的主键和外键再想想如果不用外键、全靠应用层逻辑判断会发生什么诸如脏数据、悬空引用之类的问题。反复画过一遍后你才会真正理解外键约束存在的意义。2.2 主键、外键、NULL的细节陷阱主键有两个硬性要求唯一且非空。这里有一个容易漏的点就是和UNIQUE约束的区别——UNIQUE列允许NULL多个NULL不算重复而主键列压根不允许NULL。MySQL的InnoDB里主键还会自动建聚簇索引这也意味着磁盘上的数据行物理排列顺序会跟主键密切相关。这些细节远不是建表工具自动生成的SQL能体现的。外键的语义也值得抠一抠。外键定义的是一个引用完整性约束比如选课表里的student_id引用学生表的id。我见过很多实际项目为了“省事”故意不建外键只靠应用层代码保证一致性看似灵活实际上在高并发写入时很容易出现孤儿数据排查起来想死。Andy课上提到的ON DELETE CASCADE / SET NULL / RESTRICT这些外键动作选项其实就是数据库在帮你处理引用关系变化时的决策这比全交给应用层更可靠。还有一个绕不开的话题就是NULL。关系模型里NULL表示“未知”或“不存在”它不是一个值。这就导致SQL里所有比较都得处理三值逻辑TRUE、FALSE、UNKNOWN。很多老手写SQL偶尔踩坑就是因为忘了NULL参与比较时可能直接让整条WHERE条件被过滤掉。最典型的就是NOT IN子查询遇到NULL返回空结果的问题我见过不止一个团队为这个线上事故挠头。后面第二讲还会再提但第一讲就应该建立起“NULL是传染源”的警觉。2.3 关系代数SQL的“隐形执行计划”lec1里Andy花了不少篇幅讲关系代数包括选择selectσ、投影projectπ、聚合、重命名以及集合运算并、交、差和连接join。这些希腊符号第一次见时很容易劝退但按我自己的经验它们其实是在教你一种“SQL背后发生了什么”的思考方式。举一个实际例子SELECT S.name FROM Student S JOIN Takes T ON S.id T.student_id WHERE T.course database; 如果用关系代数写逻辑顺序是先从Student表做重命名得到S从Takes表重命名得到T接着做连接π(σ(S ⋈ T))注意选择和投影的执行顺序不同可能得到完全不同的成本和结果数。查询优化器就是在这些等价表达式中间挑最优的。我后来的体会是把关系代数看作“数据库世界的流程图”。当你学会把一条SQL拆成一棵代数树你才能理解为什么有的SQL写法会触发慢查询——因为你的写法可能迫使优化器做笛卡尔积再过滤而不是先过滤再连接。Andy在前三讲不直接讲优化器但他埋的伏笔就在这。实操建议给自己出十道关系代数题比如“找出选了所有课程的学生”“找出选修了同一位老师两门课的学生”先用SQL写再尝试翻译成关系代数。你会发现很多用中文很好描述的需求翻译成代数表达式时逼着你把精确定义想清楚。这个过程非常磨练对SQL语义的理解。3. 第2讲核心SQL的声明式思维与执行顺序3.1 SQL分类与表定义里的约束优先级lec2从一个很舒服的切入点展开SQL分DDL数据定义、DML数据操作、DCL数据控制三类。最核心的是CREATE TABLE其中涉及的约束特别多PRIMARY KEY、FOREIGN KEY、UNIQUE、NOT NULL、DEFAULT、CHECK。我整理笔记时给它们分了个优先级视角主键决定行的身份外键决定行之间的血缘CHECK列级约束则是给单列上保险丝。Andy在课上特意强调了一个观点关系模型虽然是理论但商业数据库的SQL实现各有差异。比如VARCHAR长度的语义PostgreSQL里VARCHAR(n)超过n会报错SQLite却不强制校验长度SQLite比较宽容不报错只是隐式截断或直接忽略长度限制。我的建议是建表前先查官方文档别靠印象。过去我在一个项目里用MySQL的VARCHAR(255)当默认模板结果后来发现某个列需要存很长的JSON恰好踩到了行大小限制的坑折腾了一晚上才定位。这些都反映出对DDL语义的理解不能只停留在“能建表就行”。另外表中列的顺序在大部分DBMS里不是随便定的它影响记录在磁盘上的物理布局。这也是从第一讲延续到第三讲的一条暗线——你以为你只是在写SQL其实每个列定义都在为存储层设计挖坑或铺路。3.2 SELECT执行顺序最值得背下来的那张表lec2花了很多时间讲SELECT语句的完整语义顺序。网上的执行顺序版本五花八门我自己整理成这张表顺序关键字/步骤说明1FROM JOIN确定数据来源做笛卡尔积再过滤连接条件2WHERE行级过滤不能使用聚合函数3GROUP BY分组4HAVING分组后过滤可以使用聚合函数5SELECT投影列计算表达式生成别名6DISTINCT去重7ORDER BY排序8LIMIT/OFFSET截断行数这张表最大的用处不是应付考试而是帮你排查SQL报错。最常见的错就是在WHERE里用COUNT(*)或者在SELECT里用了一个没有GROUP BY的普通列。理解了执行顺序这类报错你根本不会犯因为你一眼就能看出某个列在分组之后根本不可见。举个例子SELECT department, COUNT() FROM employees WHERE salary 8000 GROUP BY department HAVING COUNT() 5 ORDER BY COUNT(*) DESC LIMIT 3。按上面的顺序走一遍先从employees经过WHERE过滤然后按部门分组统计每个组的人数HAVING过滤掉少于5人的组SELECT输出部门和人数排序后取前三。每一步都有明确的数据边界逻辑就不会乱。3.3 JOIN、聚合与子查询的实战避坑JOIN是SQL里的高频热点但真正容易出错的地方在于外连接时NULL的处理。LEFT JOIN右表没有匹配行时右表所有列都以NULL填充此时如果你在WHERE里写了右表某个字段的条件比如t.course database会把原本保留的左表行又过滤掉——很多人所谓“LEFT JOIN结果少了”基本都是这个原因。正确的做法是对右表的过滤条件放在ON子句里而不是WHERE子句里。这个规则我建议直接记成一句口诀外连接的保留行过滤条件在ON最终结果过滤条件在WHERE。Andy的课虽然没有像MySQL手册那样单列这一条但你在做完Project 3的Join算子之后会从原理上彻底明白这一点。聚合函数也有一个值得注意的坑COUNT(column)和COUNT()不一样。COUNT(column)会忽略NULL行COUNT()统计所有行。项目里统计订单数时如果用COUNT(coupon_id)明明有一堆没有用券的订单结果数量神秘变小排查起来非常隐蔽。子查询这块我自己始终提醒自己的是能用JOIN表达的查询尽量别用相关的子查询。相关子查询往往会让外部每一行都触发一次内部查询性能容易爆掉。不过这次说的性能问题是泛指它跟优化器实现有关不能一概而论。关键是建好索引、多看看执行计划。我建议跟读lec2时顺手把每一个SQL例子都在数据库里跑一遍特别是尝试用不同写法完成同一个需求子查询vs JOIN vs EXISTS看看执行计划有什么不同。这种对比练习积累多了写SQL才能又快又准。4. 第3讲核心数据库怎么在磁盘上“摆”数据4.1 从存储层次到页的必然性lec3一上来就丢了一个核心背景数据库要面对的是磁盘或SSD和内存之间的巨大差异。内存访问按纳秒算随机磁盘访问按毫秒算SSD也在几十微秒到几百微秒这个量级。这意味着数据库不可能每处理一条记录就去磁盘上找一次必须把磁盘上的数据组织成固定大小的“页”page以页为单位读写。通常一个页是4KBPostgreSQL默认8KBMySQL的InnoDB默认16KB。这就像你搬家时不可能把每件衣服单独运一趟一定装箱、打包、整车运输。页就是数据库的“箱子”。所有数据库系统都围绕这个箱子做文章内存里的缓存是页的缓存索引的节点是一个个页事务提交时刷盘也是以页为单位做日志和落盘。理解了这一点再看存储设计就顺了数据库本质上是“一个按页管理文件的软件”所有上层功能都得落实成“把Page #N放进内存 / 把Page #N写回磁盘”这样的操作。Andy在lec3反复强调“DBMS aims to maximize the number of pages in memory”其实就是点出后续缓冲区管理器buffer pool的全部意义。4.2 堆文件、链表 vs 页目录文件组织方式lec3重点讲了堆文件heap file的两种实现链表式和页目录式。链表式就是每个页存着下一页的位置逻辑简单但想要找到某个页就得从头遍历随机访问性能不好。页目录式专门维护一个页目录页相当于一个索引表记录每个页的位置能快速找到一个页。实操联想日常的文件系统里树状目录和扁平文件各有优劣数据库的文件组织也是同样的权衡。课程后面还有专题页如B树索引页、哈希页都属于不同形式的数据组织。在笔记里我把这三层概念串起来数据文件是页的集合页是记录的容器记录是行的实际编码。这三层关系如果你能闭眼画出来lec3就算过关了。Andy课堂上还会提到“页头page header”保存元数据比如页的编号、空闲空间起始位置、记录数量、校验和等。每个数据库页都有自己的头部千万别把页头和表格头搞混——页面头是物理存储层的东西表格头是逻辑数据模型层面的东西两者在不同层级解决不同问题。4.3 Slotted Page变长记录怎么安家lec3中最具实操价值的概念就是Slotted Page槽位页。它的设计是在页的头部放一个槽位数组每个槽位记录对应记录在页内的偏移量记录从页末尾往前生长槽位数组从页头往后增长。当删除一条记录时只是把槽位标记为空或移除文件本身不立即压缩当插入新记录时如果后面空闲空间不足DBMS可能选择移动已有记录或找新页。这套设计最直接的实际价值体现在数据库的UPDATE大多不是原地修改尤其是变长字段从短变长时原位置放不下数据库可能把整条记录搬到新页或者做“先删除后插入”的物理变化。理解这一点的人在设计表结构时会更有意识地区分定长字段和变长字段而不是所有列都一律VARCHAR(255)——因为在页内定长记录可以靠偏移量“秒定位”变长则需要读槽位、跟随指针成本完全不同。我强烈建议自己画一张图左边是页头槽位数组右边是变长记录区域中间是空洞标出每一条记录和它对应的槽位偏移。这个图看懂后再去理解PostgreSQL的MVCC为什么需要“过期版本”在不同页里循环布局的问题就会贯通起来。4.4 记录内部布局NULL位图与定长/变长字段记录内部的布局lec3也给出详细分析。定长记录在页内不需要额外存储元数据就能算出偏移比如一条记录有3个INT字段每条占12字节那第4条记录的起点就是第36字节。变长记录就不一样它需要保存字段的长度信息或结束位置。和开发经验对应起来你为什么经常看到“SELECT *”性能差因为你可能把一个大VARCHAR或TEXT字段也读出来了记录长度变大每页装的记录变少IO放大。明白了记录内部布局后你就知道该只select需要的列避免把大字段拖进page里浪费IO。NULL的处理也是lew3的重点。一些系统用“NULL位图”null bitmap记录哪些字段为NULL这比在每一行的数据里存一个NULL标记更省空间。MySQL的InnoDB记录头也会包含NULL标志PostgreSQL则使用HEAP Header加上null bitmap机制。总之NULL在存储层面并不是你想象的“空字符”它更接近一个元数据标记。一个细节让我印象很深如果表里所有字段都是NOT NULLDBMS可能不需要为NULL维护任何额外信息这样每行能省下不少字节。对应到建表实践就是要明确区分“业务上允许空”和“仅仅是当时没填”不要把可空性当摆设。该NOT NULL的一定加上既规范数据又能变相节省存储并提升扫描性能。5. 常见问题与排查技巧实录5.1 我在前三讲踩过的理解误区第一坑把“关系”理解成表之间的关联。实际上关系relation就是表关系模型中的“关系代数”和“关系关联查询”没有直接关系。这个误区会导致看外键定义和JOIN语义时脑子容易乱。第二坑以为SQL执行顺序和书写顺序一致。这是我早期写复杂SQL时反复头疼的根源。一旦背熟了上文的执行顺序表哪些别名能用在哪些子句里、WHERE能不能用聚合函数全都迎刃而解了。第三坑把“页头”“记录头”当成一种东西。二者层级完全不同前者是物理存储单元的头信息后者是单条记录内部的编码头信息。理解它们的前提是先分清楚“页”和“记录”是两个抽象层级。5.2 学习过程中的演练方式我的一个做法是找一张自己的真实表结构比如用户订单表把建表DDL、几条INSERT语句以及磁盘上大致怎么分布的过程都走一遍。然后分别设定几个场景插入一条很长的地址字段、删除一条记录、把一个改得很长的备注字段更新进去。设想DBMS会怎么操作页和槽位。这个过程不需要真去读MySQL的源码但会让你对为何要控制字段长度、为何页空间会碎片化等有直观认识。另一个实用手段给每讲做一个“一页纸总结”。Lec1一页纸写上关系模型三组件和主外键约束Lec2一页纸写SQL执行顺序和JOIN陷阱Lec3一页纸画slotted page图。复盘时只需要看这三页纸效率高得多。5.3 环境与练习资源建议环境方面我建议装两个东西SQLite零配置适合快速验证SQL语义和PostgreSQL和cmu15445实验更贴近的关系型数据库也是一线使用率很高的开源数据库。用SQLite跑一个单独的文件用PostgreSQL跑更严格的约束校验对比两者对相同SQL处理上的差异帮助很大。另外课程官网和公开仓库里有配套的PPT、作业说明、往年考试题。前三讲对应的Project 0虽然没正式发布但相关的C基础题目值得先热身。每周群里总有人问“如何入门cmu15445”我的回复永远是练练练。凡是你觉得自己看懂了就用代码跑一遍跑不出来就是没懂。5.4 前三讲如何为后续学习铺路lec4会讲缓冲区池Buffer Pool它是整门课的第一道分水岭。lec3的页和文件组织是理解buffer pool的前提。lec5-6讲哈希索引和B树它们的存储载体还是页结构。lec7-8讲排序和连接算法连接算子读取左右子树数据的单位就是页。lec9-10讲查询优化优化的基础就是关系代数等价变换lec2埋的点。至于lec11以后的事务和并发控制又依赖前面对记录布局和锁的语义的理解。所以大家在学前三讲时心里要有路线图你现在看到的一切“为什么存储要这样设计”都是后面讨论“怎么加速”和“怎么保证正确”的地基。6. 对初学者的话怎么把这一遍学扎实6.1 我在实际学习中的节奏安排我自己的试错经验是不要一晚上连看三讲。前三讲信息密度太高连看之后大概率只是“听过”。更有效的节奏是第一遍倍速观看建立整体框架第二遍正常速度细看边看边记笔记第三遍隔一天后不看视频自己复述每节课的核心逻辑。我第一次连看三讲之后第二天发现连slotted page的槽位数组方向都画反了。后来用“复述法”才真正解决问题。学习这种硬核课程大脑需要“睡眠巩固”千万不要图快。6.2 关于Project 0和动手实验的告诫虽然前三讲看似没有正式Project但Andy自己会建议你提前掌握C的一些基本工具链CMake、Sanitizer、Google Test。这些基本功会直接影响后面Project 1的完成速度。我见过不少同学卡在环境配置上根本不是算法不会而是不会看CMake输出、不会用ASAN排查内存越界。给自己定一个小目标在进入lec4前能用C写出一个把任意结构体序列化到固定大小缓冲区、再从缓冲区解析出来的小工具。这样等到Project 1要做Disk Manager时你的思路会顺很多。7. 后续内容可以这样扩展7.1 从前三讲延伸到缓冲区管理一旦你真正理解了页和槽位再看Buffer Pool时学生会非常自然DBMS在内存里维护若干frame每个frame对应磁盘上的一个page通过page table记录映射关系使用clock或LRU策略衡量驱逐哪些页。你会发现lec3是在回答“数据长什么样”lec4是在回答“数据怎么在内存和磁盘间流动”。建议学完lec3后先自己写一段伪代码给定一个页ID先从page table查它是否在内存中不在就从磁盘读入然后计算它内部有没有空闲空间能插入一条记录。这段伪代码写出来你对后面的并发控制为什么会需要闩锁latch也会更有体会。因为多个线程同时读写同一个页时buffer pool的页就是天然的竞争资源。7.2 从关系代数延伸到查询优化lec2学的选择、投影、连接、聚合刚好是查询优化器做等价变换的最小集合。后面讲启发式优化时会教你如何把选择尽量下推、把投影尽量下推。如果你没有建立“代数表达式树”的心智模型优化器就是在黑盒变戏法。所以我把关系代数从“SQL前置知识”提升到了“优化器的母语”这个高度来学。7.3 从页布局延伸到真实存储引擎的差异学完前三讲你可以做一个小调研对比PostgreSQL8KB页、堆表、MVCC和MySQL InnoDB16KB页、聚簇索引、undo log在页与记录组织上的关键差异。这比单纯背八股文更有用面试官如果问你“为什么InnoDB用聚簇索引而PostgreSQL用堆表”你完全可以从页布局和记录定位方式的角度给出结构性的回答。我也把这个话题定位成“学完lec3之后最好的延伸讨论”因为逻辑上PostgreSQL记录通过页号槽号定位表本身是一个堆而InnoDB的主键索引叶子节点直接存放整行记录。这些差异其实就是lec3里“堆文件组织”和“索引组织表”两种思路在工业界的实体化。在课程里Andy会明确说“我们还没有讲到索引”所以你现在看到堆组织就可以了。但结合真实数据库对比着看学习体验会立刻变得立体——这也是为什么我一直强调课程和工程文档要配合食用不要只看一边。