教学数据库项目地址: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 就是数据库里常见的 RecordId 或 RID。
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。理解了这些,才能真正看懂一条记录在数据库页里是怎么存放、移动、删除和更新的。