一张表在磁盘上长什么样:Heap File 入门

教学数据库项目地址:MiniDB(欢迎参与完善,可以给个star/fork)

上一篇讲了数据库为什么要用 Page。

Page 是数据库存储层的基本读写单位。数据库不会每次只读写一行,而是把数据组织成固定大小的页,比如 MiniDB 里使用的 8KB page。

但知道了 Page 还不够。下一个问题是:一张表到底怎么放在磁盘上?

比如有一张表:

sql 复制代码
CREATE TABLE users (
    id INT,
    name TEXT,
    age INT
);

插入几行数据:

sql 复制代码
INSERT INTO users VALUES (1, 'Alice', 20);
INSERT INTO users VALUES (2, 'Bob', 21);
INSERT INTO users VALUES (3, 'Cindy', 22);

这些数据最终不会以"表格"的样子存在磁盘上。磁盘上没有 Excel 那种行列结构。数据库会把这张表拆成很多 page,再把 tuple 放进这些 page 里。

这一组 page 组成的文件,通常就叫 Heap File

Heap File 是什么

Heap File 可以先简单理解成:一张表对应的一组数据页

它不要求数据按主键排序,也不要求按某个索引顺序排列。新插入的 tuple 通常会被放到某个还有空间的 page 里。如果没有合适的 page,就分配新 page。

大致结构是这样:
#mermaid-svg-J9lvwQDgw30Zwu0M{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-J9lvwQDgw30Zwu0M .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-J9lvwQDgw30Zwu0M .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-J9lvwQDgw30Zwu0M .error-icon{fill:#552222;}#mermaid-svg-J9lvwQDgw30Zwu0M .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-J9lvwQDgw30Zwu0M .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-J9lvwQDgw30Zwu0M .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-J9lvwQDgw30Zwu0M .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-J9lvwQDgw30Zwu0M .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-J9lvwQDgw30Zwu0M .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-J9lvwQDgw30Zwu0M .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-J9lvwQDgw30Zwu0M .marker{fill:#333333;stroke:#333333;}#mermaid-svg-J9lvwQDgw30Zwu0M .marker.cross{stroke:#333333;}#mermaid-svg-J9lvwQDgw30Zwu0M svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-J9lvwQDgw30Zwu0M p{margin:0;}#mermaid-svg-J9lvwQDgw30Zwu0M .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-J9lvwQDgw30Zwu0M .cluster-label text{fill:#333;}#mermaid-svg-J9lvwQDgw30Zwu0M .cluster-label span{color:#333;}#mermaid-svg-J9lvwQDgw30Zwu0M .cluster-label span p{background-color:transparent;}#mermaid-svg-J9lvwQDgw30Zwu0M .label text,#mermaid-svg-J9lvwQDgw30Zwu0M span{fill:#333;color:#333;}#mermaid-svg-J9lvwQDgw30Zwu0M .node rect,#mermaid-svg-J9lvwQDgw30Zwu0M .node circle,#mermaid-svg-J9lvwQDgw30Zwu0M .node ellipse,#mermaid-svg-J9lvwQDgw30Zwu0M .node polygon,#mermaid-svg-J9lvwQDgw30Zwu0M .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-J9lvwQDgw30Zwu0M .rough-node .label text,#mermaid-svg-J9lvwQDgw30Zwu0M .node .label text,#mermaid-svg-J9lvwQDgw30Zwu0M .image-shape .label,#mermaid-svg-J9lvwQDgw30Zwu0M .icon-shape .label{text-anchor:middle;}#mermaid-svg-J9lvwQDgw30Zwu0M .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-J9lvwQDgw30Zwu0M .rough-node .label,#mermaid-svg-J9lvwQDgw30Zwu0M .node .label,#mermaid-svg-J9lvwQDgw30Zwu0M .image-shape .label,#mermaid-svg-J9lvwQDgw30Zwu0M .icon-shape .label{text-align:center;}#mermaid-svg-J9lvwQDgw30Zwu0M .node.clickable{cursor:pointer;}#mermaid-svg-J9lvwQDgw30Zwu0M .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-J9lvwQDgw30Zwu0M .arrowheadPath{fill:#333333;}#mermaid-svg-J9lvwQDgw30Zwu0M .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-J9lvwQDgw30Zwu0M .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-J9lvwQDgw30Zwu0M .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J9lvwQDgw30Zwu0M .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-J9lvwQDgw30Zwu0M .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J9lvwQDgw30Zwu0M .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-J9lvwQDgw30Zwu0M .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-J9lvwQDgw30Zwu0M .cluster text{fill:#333;}#mermaid-svg-J9lvwQDgw30Zwu0M .cluster span{color:#333;}#mermaid-svg-J9lvwQDgw30Zwu0M div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-J9lvwQDgw30Zwu0M .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-J9lvwQDgw30Zwu0M rect.text{fill:none;stroke-width:0;}#mermaid-svg-J9lvwQDgw30Zwu0M .icon-shape,#mermaid-svg-J9lvwQDgw30Zwu0M .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J9lvwQDgw30Zwu0M .icon-shape p,#mermaid-svg-J9lvwQDgw30Zwu0M .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-J9lvwQDgw30Zwu0M .icon-shape .label rect,#mermaid-svg-J9lvwQDgw30Zwu0M .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J9lvwQDgw30Zwu0M .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-J9lvwQDgw30Zwu0M .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-J9lvwQDgw30Zwu0M :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} users table
Heap File
Page 0

8KB
Page 1

8KB
Page 2

8KB
slot 0: user id=1
slot 1: user id=2
slot 0: user id=3

这个结构里有几个重点:

text 复制代码
一张表不是一个连续数组
一张表由很多 page 组成
每个 page 里有多个 slot
每个 slot 指向一条 tuple
tuple 的位置由 page_id + slot_id 定位

这个 page_id + slot_id 就是数据库里常见的 RecordIdRID

RID:数据库内部定位一行的方式

应用层看一行数据,通常会用主键:

sql 复制代码
SELECT * FROM users WHERE id = 1;

但数据库内部不一定直接用主键定位物理位置。主键只是逻辑概念。真正找到 tuple 时,底层需要知道它在哪个 page、哪个 slot。

所以很多数据库会使用类似这样的地址:

text 复制代码
RID = page_id + slot_id

例如:

text 复制代码
RID(page_id=3, slot_id=7)

意思是:

text 复制代码
去第 3 个 page
找到第 7 个 slot
slot 指向那条 tuple

用 Mermaid 表示:
#mermaid-svg-rp6TuWbPtrpVZD9D{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rp6TuWbPtrpVZD9D .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rp6TuWbPtrpVZD9D .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rp6TuWbPtrpVZD9D .error-icon{fill:#552222;}#mermaid-svg-rp6TuWbPtrpVZD9D .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rp6TuWbPtrpVZD9D .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rp6TuWbPtrpVZD9D .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rp6TuWbPtrpVZD9D .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rp6TuWbPtrpVZD9D .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rp6TuWbPtrpVZD9D .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rp6TuWbPtrpVZD9D .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rp6TuWbPtrpVZD9D .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rp6TuWbPtrpVZD9D .marker.cross{stroke:#333333;}#mermaid-svg-rp6TuWbPtrpVZD9D svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rp6TuWbPtrpVZD9D p{margin:0;}#mermaid-svg-rp6TuWbPtrpVZD9D .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rp6TuWbPtrpVZD9D .cluster-label text{fill:#333;}#mermaid-svg-rp6TuWbPtrpVZD9D .cluster-label span{color:#333;}#mermaid-svg-rp6TuWbPtrpVZD9D .cluster-label span p{background-color:transparent;}#mermaid-svg-rp6TuWbPtrpVZD9D .label text,#mermaid-svg-rp6TuWbPtrpVZD9D span{fill:#333;color:#333;}#mermaid-svg-rp6TuWbPtrpVZD9D .node rect,#mermaid-svg-rp6TuWbPtrpVZD9D .node circle,#mermaid-svg-rp6TuWbPtrpVZD9D .node ellipse,#mermaid-svg-rp6TuWbPtrpVZD9D .node polygon,#mermaid-svg-rp6TuWbPtrpVZD9D .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rp6TuWbPtrpVZD9D .rough-node .label text,#mermaid-svg-rp6TuWbPtrpVZD9D .node .label text,#mermaid-svg-rp6TuWbPtrpVZD9D .image-shape .label,#mermaid-svg-rp6TuWbPtrpVZD9D .icon-shape .label{text-anchor:middle;}#mermaid-svg-rp6TuWbPtrpVZD9D .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rp6TuWbPtrpVZD9D .rough-node .label,#mermaid-svg-rp6TuWbPtrpVZD9D .node .label,#mermaid-svg-rp6TuWbPtrpVZD9D .image-shape .label,#mermaid-svg-rp6TuWbPtrpVZD9D .icon-shape .label{text-align:center;}#mermaid-svg-rp6TuWbPtrpVZD9D .node.clickable{cursor:pointer;}#mermaid-svg-rp6TuWbPtrpVZD9D .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rp6TuWbPtrpVZD9D .arrowheadPath{fill:#333333;}#mermaid-svg-rp6TuWbPtrpVZD9D .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rp6TuWbPtrpVZD9D .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rp6TuWbPtrpVZD9D .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rp6TuWbPtrpVZD9D .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rp6TuWbPtrpVZD9D .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rp6TuWbPtrpVZD9D .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rp6TuWbPtrpVZD9D .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rp6TuWbPtrpVZD9D .cluster text{fill:#333;}#mermaid-svg-rp6TuWbPtrpVZD9D .cluster span{color:#333;}#mermaid-svg-rp6TuWbPtrpVZD9D div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rp6TuWbPtrpVZD9D .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rp6TuWbPtrpVZD9D rect.text{fill:none;stroke-width:0;}#mermaid-svg-rp6TuWbPtrpVZD9D .icon-shape,#mermaid-svg-rp6TuWbPtrpVZD9D .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rp6TuWbPtrpVZD9D .icon-shape p,#mermaid-svg-rp6TuWbPtrpVZD9D .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rp6TuWbPtrpVZD9D .icon-shape .label rect,#mermaid-svg-rp6TuWbPtrpVZD9D .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rp6TuWbPtrpVZD9D .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rp6TuWbPtrpVZD9D .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rp6TuWbPtrpVZD9D :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} RID

page_id=3

slot_id=7
Page 3
Slot 7
Tuple Data

为什么不直接记录文件偏移?

比如直接记录:

text 复制代码
offset = 123456

看起来也能定位数据。但问题是,tuple 可能会因为更新、压缩、清理而在 page 内部移动。只要外部直接依赖物理偏移,tuple 一动,所有引用都要更新。

RID 的好处是:slot_id 可以保持稳定,tuple 在 page 内部怎么移动,是 page 自己的事情。

也就是说,外部只认 slot,page 内部再通过 slot 找真实 tuple。

这就是 line pointer / slot array 的价值。下一篇会专门讲 page 内部结构,这里先记住:RID 是表、索引、执行器之间传递物理位置的核心。

Heap File 为什么叫 Heap

这里的 heap 不是堆排序里的 heap,也不是程序运行时的堆内存。

在数据库语境里,heap 更接近"无序存放"的意思。

Heap File 里的数据通常不保证按主键顺序排列。比如你插入:

sql 复制代码
INSERT INTO users VALUES (10, 'Tom', 18);
INSERT INTO users VALUES (2, 'Bob', 20);
INSERT INTO users VALUES (7, 'Jack', 19);

磁盘上的物理顺序不一定是:

text 复制代码
2, 7, 10

更可能是插入顺序,或者由空闲空间决定:

text 复制代码
10, 2, 7

如果你需要按 id 快速查找,就需要索引。索引会保存 key 到 RID 的映射:

text 复制代码
id=2  -> RID(page=0, slot=1)
id=7  -> RID(page=0, slot=2)
id=10 -> RID(page=0, slot=0)

可以这样理解:
#mermaid-svg-UDql6hqWi3seo5Dy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UDql6hqWi3seo5Dy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UDql6hqWi3seo5Dy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UDql6hqWi3seo5Dy .error-icon{fill:#552222;}#mermaid-svg-UDql6hqWi3seo5Dy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UDql6hqWi3seo5Dy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UDql6hqWi3seo5Dy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UDql6hqWi3seo5Dy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UDql6hqWi3seo5Dy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UDql6hqWi3seo5Dy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UDql6hqWi3seo5Dy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UDql6hqWi3seo5Dy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UDql6hqWi3seo5Dy .marker.cross{stroke:#333333;}#mermaid-svg-UDql6hqWi3seo5Dy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UDql6hqWi3seo5Dy p{margin:0;}#mermaid-svg-UDql6hqWi3seo5Dy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UDql6hqWi3seo5Dy .cluster-label text{fill:#333;}#mermaid-svg-UDql6hqWi3seo5Dy .cluster-label span{color:#333;}#mermaid-svg-UDql6hqWi3seo5Dy .cluster-label span p{background-color:transparent;}#mermaid-svg-UDql6hqWi3seo5Dy .label text,#mermaid-svg-UDql6hqWi3seo5Dy span{fill:#333;color:#333;}#mermaid-svg-UDql6hqWi3seo5Dy .node rect,#mermaid-svg-UDql6hqWi3seo5Dy .node circle,#mermaid-svg-UDql6hqWi3seo5Dy .node ellipse,#mermaid-svg-UDql6hqWi3seo5Dy .node polygon,#mermaid-svg-UDql6hqWi3seo5Dy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UDql6hqWi3seo5Dy .rough-node .label text,#mermaid-svg-UDql6hqWi3seo5Dy .node .label text,#mermaid-svg-UDql6hqWi3seo5Dy .image-shape .label,#mermaid-svg-UDql6hqWi3seo5Dy .icon-shape .label{text-anchor:middle;}#mermaid-svg-UDql6hqWi3seo5Dy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UDql6hqWi3seo5Dy .rough-node .label,#mermaid-svg-UDql6hqWi3seo5Dy .node .label,#mermaid-svg-UDql6hqWi3seo5Dy .image-shape .label,#mermaid-svg-UDql6hqWi3seo5Dy .icon-shape .label{text-align:center;}#mermaid-svg-UDql6hqWi3seo5Dy .node.clickable{cursor:pointer;}#mermaid-svg-UDql6hqWi3seo5Dy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UDql6hqWi3seo5Dy .arrowheadPath{fill:#333333;}#mermaid-svg-UDql6hqWi3seo5Dy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UDql6hqWi3seo5Dy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UDql6hqWi3seo5Dy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UDql6hqWi3seo5Dy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UDql6hqWi3seo5Dy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UDql6hqWi3seo5Dy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UDql6hqWi3seo5Dy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UDql6hqWi3seo5Dy .cluster text{fill:#333;}#mermaid-svg-UDql6hqWi3seo5Dy .cluster span{color:#333;}#mermaid-svg-UDql6hqWi3seo5Dy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UDql6hqWi3seo5Dy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UDql6hqWi3seo5Dy rect.text{fill:none;stroke-width:0;}#mermaid-svg-UDql6hqWi3seo5Dy .icon-shape,#mermaid-svg-UDql6hqWi3seo5Dy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UDql6hqWi3seo5Dy .icon-shape p,#mermaid-svg-UDql6hqWi3seo5Dy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UDql6hqWi3seo5Dy .icon-shape .label rect,#mermaid-svg-UDql6hqWi3seo5Dy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UDql6hqWi3seo5Dy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UDql6hqWi3seo5Dy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UDql6hqWi3seo5Dy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} B+Tree Index

key = id
id=2
id=7
id=10
RID(page=0, slot=1)
RID(page=0, slot=2)
RID(page=0, slot=0)
Heap File

Heap File 负责存真实数据,索引负责加速查找。

这也是关系型数据库里非常核心的分工:

text 复制代码
Heap 存 row
Index 存 key -> row location

MiniDB 也是这种设计。表数据放在 heap storage 里,B+Tree 索引保存 IndexKey -> RecordId 的映射。

插入一行时,Heap File 做了什么

现在看一次插入。

sql 复制代码
INSERT INTO users VALUES (4, 'David', 23);

从存储层看,它不是简单 append 到文件末尾。Heap File 需要做几件事。

第一,找一个有足够空间的 page。

text 复制代码
这个 page 还能不能放下一条 tuple?
如果可以,放进去。
如果不可以,找下一个 page。
如果都不行,分配新 page。

第二,在 page 里分配一个 slot。

text 复制代码
slot_id = page.allocate_slot()

第三,把 tuple 写入 page 内部的空闲区域。

第四,返回 RID。

text 复制代码
RID(page_id, slot_id)

这条 RID 会被上层用来维护索引。比如这张表有一个 id 索引,那么插入 heap 后,还要往 B+Tree 里插入:

text 复制代码
IndexKey(id=4) -> RID(page_id, slot_id)

流程大致是:
#mermaid-svg-lpmVUq2c1wfp4j9z{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lpmVUq2c1wfp4j9z .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lpmVUq2c1wfp4j9z .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lpmVUq2c1wfp4j9z .error-icon{fill:#552222;}#mermaid-svg-lpmVUq2c1wfp4j9z .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lpmVUq2c1wfp4j9z .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lpmVUq2c1wfp4j9z .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lpmVUq2c1wfp4j9z .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lpmVUq2c1wfp4j9z .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lpmVUq2c1wfp4j9z .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lpmVUq2c1wfp4j9z .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lpmVUq2c1wfp4j9z .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lpmVUq2c1wfp4j9z .marker.cross{stroke:#333333;}#mermaid-svg-lpmVUq2c1wfp4j9z svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lpmVUq2c1wfp4j9z p{margin:0;}#mermaid-svg-lpmVUq2c1wfp4j9z .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lpmVUq2c1wfp4j9z .cluster-label text{fill:#333;}#mermaid-svg-lpmVUq2c1wfp4j9z .cluster-label span{color:#333;}#mermaid-svg-lpmVUq2c1wfp4j9z .cluster-label span p{background-color:transparent;}#mermaid-svg-lpmVUq2c1wfp4j9z .label text,#mermaid-svg-lpmVUq2c1wfp4j9z span{fill:#333;color:#333;}#mermaid-svg-lpmVUq2c1wfp4j9z .node rect,#mermaid-svg-lpmVUq2c1wfp4j9z .node circle,#mermaid-svg-lpmVUq2c1wfp4j9z .node ellipse,#mermaid-svg-lpmVUq2c1wfp4j9z .node polygon,#mermaid-svg-lpmVUq2c1wfp4j9z .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lpmVUq2c1wfp4j9z .rough-node .label text,#mermaid-svg-lpmVUq2c1wfp4j9z .node .label text,#mermaid-svg-lpmVUq2c1wfp4j9z .image-shape .label,#mermaid-svg-lpmVUq2c1wfp4j9z .icon-shape .label{text-anchor:middle;}#mermaid-svg-lpmVUq2c1wfp4j9z .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lpmVUq2c1wfp4j9z .rough-node .label,#mermaid-svg-lpmVUq2c1wfp4j9z .node .label,#mermaid-svg-lpmVUq2c1wfp4j9z .image-shape .label,#mermaid-svg-lpmVUq2c1wfp4j9z .icon-shape .label{text-align:center;}#mermaid-svg-lpmVUq2c1wfp4j9z .node.clickable{cursor:pointer;}#mermaid-svg-lpmVUq2c1wfp4j9z .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lpmVUq2c1wfp4j9z .arrowheadPath{fill:#333333;}#mermaid-svg-lpmVUq2c1wfp4j9z .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lpmVUq2c1wfp4j9z .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lpmVUq2c1wfp4j9z .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lpmVUq2c1wfp4j9z .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lpmVUq2c1wfp4j9z .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lpmVUq2c1wfp4j9z .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lpmVUq2c1wfp4j9z .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lpmVUq2c1wfp4j9z .cluster text{fill:#333;}#mermaid-svg-lpmVUq2c1wfp4j9z .cluster span{color:#333;}#mermaid-svg-lpmVUq2c1wfp4j9z div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lpmVUq2c1wfp4j9z .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lpmVUq2c1wfp4j9z rect.text{fill:none;stroke-width:0;}#mermaid-svg-lpmVUq2c1wfp4j9z .icon-shape,#mermaid-svg-lpmVUq2c1wfp4j9z .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lpmVUq2c1wfp4j9z .icon-shape p,#mermaid-svg-lpmVUq2c1wfp4j9z .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lpmVUq2c1wfp4j9z .icon-shape .label rect,#mermaid-svg-lpmVUq2c1wfp4j9z .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lpmVUq2c1wfp4j9z .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lpmVUq2c1wfp4j9z .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lpmVUq2c1wfp4j9z :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} yes
no
INSERT tuple
Find page with free space
Has enough space?
Use existing page
Allocate new page
Allocate slot
Write tuple data
Return RID
Insert index entries

这就是为什么插入一行数据不只是"写文件"。它至少涉及:

text 复制代码
HeapFile
Page
Slot
RID
Index
WAL
Transaction
BufferPool

这里先只讲 HeapFile,后面会继续把 WAL、索引和事务补上。

删除一行时,Heap File 不能马上把它抹掉

删除看起来也很简单:

sql 复制代码
DELETE FROM users WHERE id = 2;

如果是普通文件,我们可能会直接把这段数据删掉。但数据库不能这么做,尤其是在支持 MVCC 的数据库里。

原因是:可能还有旧事务正在读这条数据。

例如:

text 复制代码
T1 开始事务,读取 users
T2 删除 id=2 并提交
T1 再次读取 users

在 Snapshot Isolation 下,T1 应该还能看到它事务开始时可见的数据。也就是说,虽然 T2 已经删除了 id=2,但对 T1 来说,这条旧版本仍然存在。

所以删除通常不会立刻物理移除 tuple,而是标记它被哪个事务删除了。

在 MVCC 系统里,一条 tuple 可能有:

text 复制代码
xmin:创建它的事务
xmax:删除或失效它的事务

删除时设置 xmax

text 复制代码
tuple.xmax = deleting_txn_id

这条 tuple 什么时候真的可以清理?

要等到没有任何旧 snapshot 还可能看到它。

流程可以理解成:
#mermaid-svg-wre3IJKZtGw03cCS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wre3IJKZtGw03cCS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wre3IJKZtGw03cCS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wre3IJKZtGw03cCS .error-icon{fill:#552222;}#mermaid-svg-wre3IJKZtGw03cCS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wre3IJKZtGw03cCS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wre3IJKZtGw03cCS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wre3IJKZtGw03cCS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wre3IJKZtGw03cCS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wre3IJKZtGw03cCS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wre3IJKZtGw03cCS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wre3IJKZtGw03cCS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wre3IJKZtGw03cCS .marker.cross{stroke:#333333;}#mermaid-svg-wre3IJKZtGw03cCS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wre3IJKZtGw03cCS p{margin:0;}#mermaid-svg-wre3IJKZtGw03cCS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wre3IJKZtGw03cCS .cluster-label text{fill:#333;}#mermaid-svg-wre3IJKZtGw03cCS .cluster-label span{color:#333;}#mermaid-svg-wre3IJKZtGw03cCS .cluster-label span p{background-color:transparent;}#mermaid-svg-wre3IJKZtGw03cCS .label text,#mermaid-svg-wre3IJKZtGw03cCS span{fill:#333;color:#333;}#mermaid-svg-wre3IJKZtGw03cCS .node rect,#mermaid-svg-wre3IJKZtGw03cCS .node circle,#mermaid-svg-wre3IJKZtGw03cCS .node ellipse,#mermaid-svg-wre3IJKZtGw03cCS .node polygon,#mermaid-svg-wre3IJKZtGw03cCS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wre3IJKZtGw03cCS .rough-node .label text,#mermaid-svg-wre3IJKZtGw03cCS .node .label text,#mermaid-svg-wre3IJKZtGw03cCS .image-shape .label,#mermaid-svg-wre3IJKZtGw03cCS .icon-shape .label{text-anchor:middle;}#mermaid-svg-wre3IJKZtGw03cCS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wre3IJKZtGw03cCS .rough-node .label,#mermaid-svg-wre3IJKZtGw03cCS .node .label,#mermaid-svg-wre3IJKZtGw03cCS .image-shape .label,#mermaid-svg-wre3IJKZtGw03cCS .icon-shape .label{text-align:center;}#mermaid-svg-wre3IJKZtGw03cCS .node.clickable{cursor:pointer;}#mermaid-svg-wre3IJKZtGw03cCS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wre3IJKZtGw03cCS .arrowheadPath{fill:#333333;}#mermaid-svg-wre3IJKZtGw03cCS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wre3IJKZtGw03cCS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wre3IJKZtGw03cCS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wre3IJKZtGw03cCS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wre3IJKZtGw03cCS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wre3IJKZtGw03cCS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wre3IJKZtGw03cCS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wre3IJKZtGw03cCS .cluster text{fill:#333;}#mermaid-svg-wre3IJKZtGw03cCS .cluster span{color:#333;}#mermaid-svg-wre3IJKZtGw03cCS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wre3IJKZtGw03cCS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wre3IJKZtGw03cCS rect.text{fill:none;stroke-width:0;}#mermaid-svg-wre3IJKZtGw03cCS .icon-shape,#mermaid-svg-wre3IJKZtGw03cCS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wre3IJKZtGw03cCS .icon-shape p,#mermaid-svg-wre3IJKZtGw03cCS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wre3IJKZtGw03cCS .icon-shape .label rect,#mermaid-svg-wre3IJKZtGw03cCS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wre3IJKZtGw03cCS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wre3IJKZtGw03cCS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wre3IJKZtGw03cCS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} yes
no
DELETE tuple
Set xmax
Old snapshot may still see it?
Keep tuple in page
Mark as dead
VACUUM / pruning
Space reusable

所以 Heap File 不是简单存储容器。它必须和事务系统配合,决定 tuple 当前对谁可见。

MiniDB 的 heap storage 也支持这种 MVCC 风格:tuple 带有 xmin/xmax,事务通过 snapshot 判断版本是否可见。删除时不是立即把数据从 page 中抹掉,而是进入 MVCC 可见性判断和后续 GC/pruning 流程。

更新一行其实更像"插入新版本"

再看更新:

sql 复制代码
UPDATE users SET age = 24 WHERE id = 4;

直觉上,我们可能会把原来的 age=23 改成 age=24

但在 MVCC 数据库里,更新通常不是原地覆盖,而是:

text 复制代码
旧版本标记失效
新版本插入出来
两个版本之间建立关系

大概是:

text 复制代码
old tuple: id=4, age=23, xmax=T2
new tuple: id=4, age=24, xmin=T2

旧事务可能还需要看到旧版本,新事务应该看到新版本。
#mermaid-svg-kLTWTkwBOEIqj6sv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kLTWTkwBOEIqj6sv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kLTWTkwBOEIqj6sv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kLTWTkwBOEIqj6sv .error-icon{fill:#552222;}#mermaid-svg-kLTWTkwBOEIqj6sv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kLTWTkwBOEIqj6sv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kLTWTkwBOEIqj6sv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kLTWTkwBOEIqj6sv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kLTWTkwBOEIqj6sv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kLTWTkwBOEIqj6sv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kLTWTkwBOEIqj6sv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kLTWTkwBOEIqj6sv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kLTWTkwBOEIqj6sv .marker.cross{stroke:#333333;}#mermaid-svg-kLTWTkwBOEIqj6sv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kLTWTkwBOEIqj6sv p{margin:0;}#mermaid-svg-kLTWTkwBOEIqj6sv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kLTWTkwBOEIqj6sv .cluster-label text{fill:#333;}#mermaid-svg-kLTWTkwBOEIqj6sv .cluster-label span{color:#333;}#mermaid-svg-kLTWTkwBOEIqj6sv .cluster-label span p{background-color:transparent;}#mermaid-svg-kLTWTkwBOEIqj6sv .label text,#mermaid-svg-kLTWTkwBOEIqj6sv span{fill:#333;color:#333;}#mermaid-svg-kLTWTkwBOEIqj6sv .node rect,#mermaid-svg-kLTWTkwBOEIqj6sv .node circle,#mermaid-svg-kLTWTkwBOEIqj6sv .node ellipse,#mermaid-svg-kLTWTkwBOEIqj6sv .node polygon,#mermaid-svg-kLTWTkwBOEIqj6sv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kLTWTkwBOEIqj6sv .rough-node .label text,#mermaid-svg-kLTWTkwBOEIqj6sv .node .label text,#mermaid-svg-kLTWTkwBOEIqj6sv .image-shape .label,#mermaid-svg-kLTWTkwBOEIqj6sv .icon-shape .label{text-anchor:middle;}#mermaid-svg-kLTWTkwBOEIqj6sv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kLTWTkwBOEIqj6sv .rough-node .label,#mermaid-svg-kLTWTkwBOEIqj6sv .node .label,#mermaid-svg-kLTWTkwBOEIqj6sv .image-shape .label,#mermaid-svg-kLTWTkwBOEIqj6sv .icon-shape .label{text-align:center;}#mermaid-svg-kLTWTkwBOEIqj6sv .node.clickable{cursor:pointer;}#mermaid-svg-kLTWTkwBOEIqj6sv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kLTWTkwBOEIqj6sv .arrowheadPath{fill:#333333;}#mermaid-svg-kLTWTkwBOEIqj6sv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kLTWTkwBOEIqj6sv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kLTWTkwBOEIqj6sv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kLTWTkwBOEIqj6sv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kLTWTkwBOEIqj6sv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kLTWTkwBOEIqj6sv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kLTWTkwBOEIqj6sv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kLTWTkwBOEIqj6sv .cluster text{fill:#333;}#mermaid-svg-kLTWTkwBOEIqj6sv .cluster span{color:#333;}#mermaid-svg-kLTWTkwBOEIqj6sv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kLTWTkwBOEIqj6sv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kLTWTkwBOEIqj6sv rect.text{fill:none;stroke-width:0;}#mermaid-svg-kLTWTkwBOEIqj6sv .icon-shape,#mermaid-svg-kLTWTkwBOEIqj6sv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kLTWTkwBOEIqj6sv .icon-shape p,#mermaid-svg-kLTWTkwBOEIqj6sv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kLTWTkwBOEIqj6sv .icon-shape .label rect,#mermaid-svg-kLTWTkwBOEIqj6sv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kLTWTkwBOEIqj6sv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kLTWTkwBOEIqj6sv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kLTWTkwBOEIqj6sv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Old Version

age=23
New Version

age=24
xmax = update_txn
xmin = update_txn

这就是为什么 Heap File 需要支持 version chain。

对于 MiniDB 来说,这一点尤其重要。MiniDB 支持 version-chain traversal 和 HOT-style same-page updates。也就是说,更新并不是单纯覆盖原 tuple,而是维护版本之间的关系,并通过事务 snapshot 判断应该返回哪个版本。

Heap File 和索引的关系

Heap File 存真实 tuple。索引存 key 到 RID 的映射。

比如:

sql 复制代码
CREATE INDEX idx_users_id ON users(id);

插入一行:

sql 复制代码
INSERT INTO users VALUES (4, 'David', 23);

Heap File 返回:

text 复制代码
RID(page=5, slot=2)

索引就插入:

text 复制代码
id=4 -> RID(page=5, slot=2)

查询时:

sql 复制代码
SELECT * FROM users WHERE id = 4;

数据库可以先走索引:

text 复制代码
id=4 -> RID(page=5, slot=2)

然后再去 Heap File 取真实 tuple。
#mermaid-svg-mUY9dhkKQ97pllqV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mUY9dhkKQ97pllqV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mUY9dhkKQ97pllqV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mUY9dhkKQ97pllqV .error-icon{fill:#552222;}#mermaid-svg-mUY9dhkKQ97pllqV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mUY9dhkKQ97pllqV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mUY9dhkKQ97pllqV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mUY9dhkKQ97pllqV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mUY9dhkKQ97pllqV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mUY9dhkKQ97pllqV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mUY9dhkKQ97pllqV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mUY9dhkKQ97pllqV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mUY9dhkKQ97pllqV .marker.cross{stroke:#333333;}#mermaid-svg-mUY9dhkKQ97pllqV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mUY9dhkKQ97pllqV p{margin:0;}#mermaid-svg-mUY9dhkKQ97pllqV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mUY9dhkKQ97pllqV .cluster-label text{fill:#333;}#mermaid-svg-mUY9dhkKQ97pllqV .cluster-label span{color:#333;}#mermaid-svg-mUY9dhkKQ97pllqV .cluster-label span p{background-color:transparent;}#mermaid-svg-mUY9dhkKQ97pllqV .label text,#mermaid-svg-mUY9dhkKQ97pllqV span{fill:#333;color:#333;}#mermaid-svg-mUY9dhkKQ97pllqV .node rect,#mermaid-svg-mUY9dhkKQ97pllqV .node circle,#mermaid-svg-mUY9dhkKQ97pllqV .node ellipse,#mermaid-svg-mUY9dhkKQ97pllqV .node polygon,#mermaid-svg-mUY9dhkKQ97pllqV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mUY9dhkKQ97pllqV .rough-node .label text,#mermaid-svg-mUY9dhkKQ97pllqV .node .label text,#mermaid-svg-mUY9dhkKQ97pllqV .image-shape .label,#mermaid-svg-mUY9dhkKQ97pllqV .icon-shape .label{text-anchor:middle;}#mermaid-svg-mUY9dhkKQ97pllqV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mUY9dhkKQ97pllqV .rough-node .label,#mermaid-svg-mUY9dhkKQ97pllqV .node .label,#mermaid-svg-mUY9dhkKQ97pllqV .image-shape .label,#mermaid-svg-mUY9dhkKQ97pllqV .icon-shape .label{text-align:center;}#mermaid-svg-mUY9dhkKQ97pllqV .node.clickable{cursor:pointer;}#mermaid-svg-mUY9dhkKQ97pllqV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mUY9dhkKQ97pllqV .arrowheadPath{fill:#333333;}#mermaid-svg-mUY9dhkKQ97pllqV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mUY9dhkKQ97pllqV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mUY9dhkKQ97pllqV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mUY9dhkKQ97pllqV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mUY9dhkKQ97pllqV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mUY9dhkKQ97pllqV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mUY9dhkKQ97pllqV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mUY9dhkKQ97pllqV .cluster text{fill:#333;}#mermaid-svg-mUY9dhkKQ97pllqV .cluster span{color:#333;}#mermaid-svg-mUY9dhkKQ97pllqV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mUY9dhkKQ97pllqV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mUY9dhkKQ97pllqV rect.text{fill:none;stroke-width:0;}#mermaid-svg-mUY9dhkKQ97pllqV .icon-shape,#mermaid-svg-mUY9dhkKQ97pllqV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mUY9dhkKQ97pllqV .icon-shape p,#mermaid-svg-mUY9dhkKQ97pllqV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mUY9dhkKQ97pllqV .icon-shape .label rect,#mermaid-svg-mUY9dhkKQ97pllqV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mUY9dhkKQ97pllqV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mUY9dhkKQ97pllqV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mUY9dhkKQ97pllqV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} WHERE id = 4
B+Tree Index
RID(page=5, slot=2)
Heap File
Tuple: id=4, name=David, age=23

这也解释了为什么 RID 很重要。

索引不直接保存完整行。它通常保存 key 和 RID。RID 是索引回到 heap 的桥梁。

当然,如果查询需要的列都在索引里,数据库有时可以走 IndexOnlyScan。但在 MVCC 数据库里,IndexOnlyScan 还要处理可见性问题,不能简单地只读索引。这一块后面会单独讲。

Heap File 怎么找有空间的 page

插入 tuple 时,Heap File 需要找到一个有足够空间的 page。

最笨的办法是从头扫:

text 复制代码
Page 0 有空间吗?
Page 1 有空间吗?
Page 2 有空间吗?
...

这当然可以,但表大了以后会很慢。

现代数据库通常会维护类似 Free Space Map 的结构,记录哪些 page 还有多少空闲空间。插入时先查 FSM,快速找到可能放得下 tuple 的 page。
#mermaid-svg-sKc8kOnJCOVDrarN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sKc8kOnJCOVDrarN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sKc8kOnJCOVDrarN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sKc8kOnJCOVDrarN .error-icon{fill:#552222;}#mermaid-svg-sKc8kOnJCOVDrarN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sKc8kOnJCOVDrarN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sKc8kOnJCOVDrarN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sKc8kOnJCOVDrarN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sKc8kOnJCOVDrarN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sKc8kOnJCOVDrarN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sKc8kOnJCOVDrarN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sKc8kOnJCOVDrarN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sKc8kOnJCOVDrarN .marker.cross{stroke:#333333;}#mermaid-svg-sKc8kOnJCOVDrarN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sKc8kOnJCOVDrarN p{margin:0;}#mermaid-svg-sKc8kOnJCOVDrarN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sKc8kOnJCOVDrarN .cluster-label text{fill:#333;}#mermaid-svg-sKc8kOnJCOVDrarN .cluster-label span{color:#333;}#mermaid-svg-sKc8kOnJCOVDrarN .cluster-label span p{background-color:transparent;}#mermaid-svg-sKc8kOnJCOVDrarN .label text,#mermaid-svg-sKc8kOnJCOVDrarN span{fill:#333;color:#333;}#mermaid-svg-sKc8kOnJCOVDrarN .node rect,#mermaid-svg-sKc8kOnJCOVDrarN .node circle,#mermaid-svg-sKc8kOnJCOVDrarN .node ellipse,#mermaid-svg-sKc8kOnJCOVDrarN .node polygon,#mermaid-svg-sKc8kOnJCOVDrarN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sKc8kOnJCOVDrarN .rough-node .label text,#mermaid-svg-sKc8kOnJCOVDrarN .node .label text,#mermaid-svg-sKc8kOnJCOVDrarN .image-shape .label,#mermaid-svg-sKc8kOnJCOVDrarN .icon-shape .label{text-anchor:middle;}#mermaid-svg-sKc8kOnJCOVDrarN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sKc8kOnJCOVDrarN .rough-node .label,#mermaid-svg-sKc8kOnJCOVDrarN .node .label,#mermaid-svg-sKc8kOnJCOVDrarN .image-shape .label,#mermaid-svg-sKc8kOnJCOVDrarN .icon-shape .label{text-align:center;}#mermaid-svg-sKc8kOnJCOVDrarN .node.clickable{cursor:pointer;}#mermaid-svg-sKc8kOnJCOVDrarN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sKc8kOnJCOVDrarN .arrowheadPath{fill:#333333;}#mermaid-svg-sKc8kOnJCOVDrarN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sKc8kOnJCOVDrarN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sKc8kOnJCOVDrarN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sKc8kOnJCOVDrarN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sKc8kOnJCOVDrarN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sKc8kOnJCOVDrarN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sKc8kOnJCOVDrarN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sKc8kOnJCOVDrarN .cluster text{fill:#333;}#mermaid-svg-sKc8kOnJCOVDrarN .cluster span{color:#333;}#mermaid-svg-sKc8kOnJCOVDrarN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sKc8kOnJCOVDrarN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sKc8kOnJCOVDrarN rect.text{fill:none;stroke-width:0;}#mermaid-svg-sKc8kOnJCOVDrarN .icon-shape,#mermaid-svg-sKc8kOnJCOVDrarN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sKc8kOnJCOVDrarN .icon-shape p,#mermaid-svg-sKc8kOnJCOVDrarN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sKc8kOnJCOVDrarN .icon-shape .label rect,#mermaid-svg-sKc8kOnJCOVDrarN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sKc8kOnJCOVDrarN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sKc8kOnJCOVDrarN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sKc8kOnJCOVDrarN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} yes
no
New tuple
Free Space Map
Candidate page
Enough space?
Write tuple
Try another page / allocate new page

MiniDB 的 heap files 也有 FSM,用来辅助插入时选择 page。这比每次从头扫描 heap file 更接近真实数据库的做法。

Heap File 需要和 Buffer Pool 配合

Heap File 自己并不直接操作磁盘文件。它通常会通过 Buffer Pool 访问 page。

读取 tuple:

text 复制代码
HeapFile 根据 RID 找到 page_id
BufferPool fetch page
HeapFile 在 page 中找到 slot
读取 tuple

插入 tuple:

text 复制代码
HeapFile 找到目标 page_id
BufferPool fetch page
修改 page
标记 dirty
之后由 BufferPool / checkpoint 刷回 PageStore

也就是说,Heap File 关心的是"表数据怎么组织",Buffer Pool 关心的是"page 怎么在内存和磁盘之间流动"。
#mermaid-svg-V74kramKEXhqU2mF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-V74kramKEXhqU2mF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-V74kramKEXhqU2mF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-V74kramKEXhqU2mF .error-icon{fill:#552222;}#mermaid-svg-V74kramKEXhqU2mF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-V74kramKEXhqU2mF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-V74kramKEXhqU2mF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-V74kramKEXhqU2mF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-V74kramKEXhqU2mF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-V74kramKEXhqU2mF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-V74kramKEXhqU2mF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-V74kramKEXhqU2mF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-V74kramKEXhqU2mF .marker.cross{stroke:#333333;}#mermaid-svg-V74kramKEXhqU2mF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-V74kramKEXhqU2mF p{margin:0;}#mermaid-svg-V74kramKEXhqU2mF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-V74kramKEXhqU2mF .cluster-label text{fill:#333;}#mermaid-svg-V74kramKEXhqU2mF .cluster-label span{color:#333;}#mermaid-svg-V74kramKEXhqU2mF .cluster-label span p{background-color:transparent;}#mermaid-svg-V74kramKEXhqU2mF .label text,#mermaid-svg-V74kramKEXhqU2mF span{fill:#333;color:#333;}#mermaid-svg-V74kramKEXhqU2mF .node rect,#mermaid-svg-V74kramKEXhqU2mF .node circle,#mermaid-svg-V74kramKEXhqU2mF .node ellipse,#mermaid-svg-V74kramKEXhqU2mF .node polygon,#mermaid-svg-V74kramKEXhqU2mF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-V74kramKEXhqU2mF .rough-node .label text,#mermaid-svg-V74kramKEXhqU2mF .node .label text,#mermaid-svg-V74kramKEXhqU2mF .image-shape .label,#mermaid-svg-V74kramKEXhqU2mF .icon-shape .label{text-anchor:middle;}#mermaid-svg-V74kramKEXhqU2mF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-V74kramKEXhqU2mF .rough-node .label,#mermaid-svg-V74kramKEXhqU2mF .node .label,#mermaid-svg-V74kramKEXhqU2mF .image-shape .label,#mermaid-svg-V74kramKEXhqU2mF .icon-shape .label{text-align:center;}#mermaid-svg-V74kramKEXhqU2mF .node.clickable{cursor:pointer;}#mermaid-svg-V74kramKEXhqU2mF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-V74kramKEXhqU2mF .arrowheadPath{fill:#333333;}#mermaid-svg-V74kramKEXhqU2mF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-V74kramKEXhqU2mF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-V74kramKEXhqU2mF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V74kramKEXhqU2mF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-V74kramKEXhqU2mF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V74kramKEXhqU2mF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-V74kramKEXhqU2mF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-V74kramKEXhqU2mF .cluster text{fill:#333;}#mermaid-svg-V74kramKEXhqU2mF .cluster span{color:#333;}#mermaid-svg-V74kramKEXhqU2mF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-V74kramKEXhqU2mF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-V74kramKEXhqU2mF rect.text{fill:none;stroke-width:0;}#mermaid-svg-V74kramKEXhqU2mF .icon-shape,#mermaid-svg-V74kramKEXhqU2mF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-V74kramKEXhqU2mF .icon-shape p,#mermaid-svg-V74kramKEXhqU2mF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-V74kramKEXhqU2mF .icon-shape .label rect,#mermaid-svg-V74kramKEXhqU2mF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-V74kramKEXhqU2mF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-V74kramKEXhqU2mF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-V74kramKEXhqU2mF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HeapFile
BufferPool
Page in memory
PageStore
Disk / PageServer

这个分层很重要。

如果没有 Buffer Pool,Heap File 每次读写都要直接访问磁盘。性能会很差,也很难处理 dirty page、WAL-first flushing、checkpoint、eviction 等问题。

MiniDB 里也是类似分层:HeapFile 通过 BufferPool 获取和修改 page,BufferPool 再通过 PageStore 读写本地文件或远程 PageServer。

Heap File 为什么不负责排序

有人可能会问:既然表里经常按主键查,为什么 Heap File 不直接按主键排序?

可以,但那就不是普通 heap storage 了,而更接近 clustered index 或 index-organized table。

Heap File 的优点是简单、通用:

text 复制代码
插入快
不依赖某个排序键
适合多索引
更新和删除可以通过 RID 定位

缺点是:

text 复制代码
按条件查找通常需要索引
按顺序扫描时没有主键顺序保证
空间清理需要 VACUUM / GC

很多数据库会选择 heap + secondary index 的组合,因为它足够通用。

MiniDB 当前也是这种路线:表数据在 heap 中,索引用 B+Tree 维护 key 到 RecordId 的映射。

Heap File 在 MiniDB 中的角色

MiniDB 的存储层大致可以这样理解:
#mermaid-svg-2c6aSWTsUVRj6OWw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2c6aSWTsUVRj6OWw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2c6aSWTsUVRj6OWw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2c6aSWTsUVRj6OWw .error-icon{fill:#552222;}#mermaid-svg-2c6aSWTsUVRj6OWw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2c6aSWTsUVRj6OWw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2c6aSWTsUVRj6OWw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2c6aSWTsUVRj6OWw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2c6aSWTsUVRj6OWw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2c6aSWTsUVRj6OWw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2c6aSWTsUVRj6OWw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2c6aSWTsUVRj6OWw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2c6aSWTsUVRj6OWw .marker.cross{stroke:#333333;}#mermaid-svg-2c6aSWTsUVRj6OWw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2c6aSWTsUVRj6OWw p{margin:0;}#mermaid-svg-2c6aSWTsUVRj6OWw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2c6aSWTsUVRj6OWw .cluster-label text{fill:#333;}#mermaid-svg-2c6aSWTsUVRj6OWw .cluster-label span{color:#333;}#mermaid-svg-2c6aSWTsUVRj6OWw .cluster-label span p{background-color:transparent;}#mermaid-svg-2c6aSWTsUVRj6OWw .label text,#mermaid-svg-2c6aSWTsUVRj6OWw span{fill:#333;color:#333;}#mermaid-svg-2c6aSWTsUVRj6OWw .node rect,#mermaid-svg-2c6aSWTsUVRj6OWw .node circle,#mermaid-svg-2c6aSWTsUVRj6OWw .node ellipse,#mermaid-svg-2c6aSWTsUVRj6OWw .node polygon,#mermaid-svg-2c6aSWTsUVRj6OWw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2c6aSWTsUVRj6OWw .rough-node .label text,#mermaid-svg-2c6aSWTsUVRj6OWw .node .label text,#mermaid-svg-2c6aSWTsUVRj6OWw .image-shape .label,#mermaid-svg-2c6aSWTsUVRj6OWw .icon-shape .label{text-anchor:middle;}#mermaid-svg-2c6aSWTsUVRj6OWw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2c6aSWTsUVRj6OWw .rough-node .label,#mermaid-svg-2c6aSWTsUVRj6OWw .node .label,#mermaid-svg-2c6aSWTsUVRj6OWw .image-shape .label,#mermaid-svg-2c6aSWTsUVRj6OWw .icon-shape .label{text-align:center;}#mermaid-svg-2c6aSWTsUVRj6OWw .node.clickable{cursor:pointer;}#mermaid-svg-2c6aSWTsUVRj6OWw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2c6aSWTsUVRj6OWw .arrowheadPath{fill:#333333;}#mermaid-svg-2c6aSWTsUVRj6OWw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2c6aSWTsUVRj6OWw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2c6aSWTsUVRj6OWw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2c6aSWTsUVRj6OWw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2c6aSWTsUVRj6OWw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2c6aSWTsUVRj6OWw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2c6aSWTsUVRj6OWw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2c6aSWTsUVRj6OWw .cluster text{fill:#333;}#mermaid-svg-2c6aSWTsUVRj6OWw .cluster span{color:#333;}#mermaid-svg-2c6aSWTsUVRj6OWw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2c6aSWTsUVRj6OWw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2c6aSWTsUVRj6OWw rect.text{fill:none;stroke-width:0;}#mermaid-svg-2c6aSWTsUVRj6OWw .icon-shape,#mermaid-svg-2c6aSWTsUVRj6OWw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2c6aSWTsUVRj6OWw .icon-shape p,#mermaid-svg-2c6aSWTsUVRj6OWw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2c6aSWTsUVRj6OWw .icon-shape .label rect,#mermaid-svg-2c6aSWTsUVRj6OWw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2c6aSWTsUVRj6OWw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2c6aSWTsUVRj6OWw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2c6aSWTsUVRj6OWw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SQL Layer
Executor
HeapFile
B+Tree Index
Heap Page
BufferPool
PageStore
TransactionManager
WALManager

HeapFile 处在一个很核心的位置。

它向上服务 SQL 执行器:

text 复制代码
SeqScan 需要从 HeapFile 扫描 tuple
InsertExecutor 需要向 HeapFile 插入 tuple
UpdateExecutor 需要在 HeapFile 中生成新版本
DeleteExecutor 需要在 HeapFile 中标记 tuple 删除

它向下依赖 BufferPool 和 PageStore:

text 复制代码
读 page
写 page
标记 dirty
刷盘

它还要和事务系统配合:

text 复制代码
判断 tuple 可见性
设置 xmin / xmax
维护版本链
支持 rollback
支持 GC / pruning

它还要和索引配合:

text 复制代码
插入 tuple 后维护 index entry
更新索引列时更新 index
删除 tuple 后处理 index 可见性

所以 HeapFile 不是一个普通文件封装。它是关系型数据库存储层的核心结构之一。

为什么这一层值得单独学习

很多人学数据库时会先看 SQL 优化器或 B+Tree,因为这些更容易被看见。

但 Heap File 其实更基础。

没有 Heap File,就没有稳定的 RID。

没有 RID,索引就不知道怎么指向真实数据。

没有 page 和 slot,tuple 的移动、删除、更新都会很麻烦。

没有 MVCC tuple header,事务就不知道谁能看到哪个版本。

没有 FSM,插入时找空闲空间会越来越慢。

没有 VACUUM,删除和更新留下的旧版本会一直膨胀。

所以 Heap File 是很多模块的交汇点:

text 复制代码
Page
Buffer Pool
Transaction
MVCC
Index
WAL
VACUUM

它看起来没有优化器那么"聪明",但数据库的正确性和可靠性很大一部分都压在这里。

小结

Heap File 是数据库表数据的基本组织方式。

它把一张表拆成很多 page,每个 page 里有多个 slot,每条 tuple 通过 page_id + slot_id 形成 RID。索引通过 RID 回到 heap 找真实数据。插入、删除、更新都不是简单文件操作,而是要和 page、Buffer Pool、MVCC、WAL、索引一起工作。

MiniDB 的存储层采用的就是这种思路:

text 复制代码
表数据放在 HeapFile
HeapFile 由多个 Page 组成
Page 内部通过 slot 管理 tuple
tuple 通过 xmin/xmax 支持 MVCC
索引保存 IndexKey -> RecordId
BufferPool 负责缓存 page
PageStore 负责本地或远程持久化

下一篇会继续往 page 内部走,讲 Page Header、Line Pointer、Slot Array 和 Tuple Data。理解了这些,才能真正看懂一条记录在数据库页里是怎么存放、移动、删除和更新的。

相关推荐
流星白龙1 小时前
【MySQL高阶】11.InnoDB存储引擎
数据库·mysql
wangbing11252 小时前
SQL Server2008 R2版自动备份问题
数据库
Trouvaille ~2 小时前
【Redis篇】Redis 渐进式遍历与数据库管理
数据库·redis·缓存·中间件·数据库管理·后端开发·scan
xcLeigh2 小时前
KES数据库运维监控与故障排查实战
运维·数据库·sql·故障排查·运维监控·kes
GlobalSign数字证书2 小时前
中小企业的 SSL/TLS 证书管理,有更轻量的方案
数据库·网络协议·ssl
梓䈑2 小时前
【MySQL】库的操作(数据库的创建、查看、修改 和 备份)
数据库·mysql
yuzhiboyouye2 小时前
原生 SQL 常用核心语句基础语法
数据库·sql·oracle
我是一颗柠檬2 小时前
【Redis】事务与Lua脚本Day7(2026年)
数据库·redis·后端·lua·database
流星白龙2 小时前
【MySQL高阶】14.MySQL存储结构
android·数据库·mysql