数据库系列(二):存储引擎 — 堆表 vs 索引组织表 vs 列存

写在前面

承接上一篇的"原理地图",本文进入第一个子系统:存储引擎。我们要回答的核心问题是——

一行数据,在磁盘上到底长什么样?为什么 MySQL InnoDB 的主键就是数据,而 PostgreSQL 不是?

理解存储引擎是理解索引、事务、MVCC、日志所有后续主题的基础。本文横向对比 Oracle、SQL Server、MySQL(InnoDB)、PostgreSQL 四家的物理存储层。


一、数据在磁盘上长什么样

1.1 朴素模型:行 + 列

逻辑上一张表就是二维结构:

1
2
3
4
5
id  | name   | age | balance
----|--------|-----|--------
1   | Alice  | 28  | 1000
2   | Bob    | 35  | 2500
3   | Carol  | 22  | 800

但磁盘上不能直接放这个二维表,需要某种物理格式。所有关系数据库的答案都是同一个——把多行打包成一个固定大小的页(Page),页是磁盘 I/O 的最小单位

1.2 为什么需要"页"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
朴素方案:一行一行存
  - 一行 100 字节,磁盘扇区 4KB
  - 读一行实际读了 4KB(40 倍浪费)
  - 写一行也要刷 4KB

正确方案:把行打包成页(如 16KB 一个)
  - 一次 I/O 读 / 写一整页
  - 内部按行寻址(offset)
  - 缓冲池管理也是以页为单位
  - 局部性原理:相邻行经常一起访问

1.3 行存 vs 列存

页内部如何组织多行?有两种思路:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
行存(Row-Based):每一行连续存储
  [id1, name1, age1, bal1][id2, name2, age2, bal2]...
  适合:SELECT * FROM t WHERE id=1  (整行取出)
       UPDATE t SET age=age+1 WHERE id=1  (改一行)
  → OLTP 场景

列存(Columnar):每一列连续存储
  id 列:  [1, 2, 3, ...]
  name 列: [Alice, Bob, Carol, ...]
  age 列: [28, 35, 22, ...]
  适合:SELECT AVG(age) FROM t  (只读 age 列)
       按列压缩率高(同质数据)
  → OLAP 场景

四大数据库默认都是行存,但 SQL Server / Oracle 都加了列存扩展用于 OLAP(详见第 5 节)。


二、物理存储单位对比

四家的命名不同,但层级结构惊人地一致:Block/Page → Extent → Segment → Tablespace

2.1 层级结构总览

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
                       Tablespace(表空间)
                       Segment(段)
                       一个表 / 索引 = 一个或多个段
                       Extent(区)
                       连续多个 Page 组成
                       Page / Block(页 / 块)
                       磁盘 I/O 最小单位
                       Row / Tuple(行 / 元组)
                       实际的数据

2.2 各家的实际大小

层级 Oracle SQL Server MySQL InnoDB PostgreSQL
Page/Block 2K/4K/8K/16K(默认 8K) 8K 16K(可配 4K/8K) 8K(编译时定)
Extent 64K(8 个 block) 64K(8 页) 1M(64 页) ——(无明确区)
Segment 表 / 索引一个段 表 / 索引 表 / 索引(每表一个段) ——(PG 用文件直接管理)
Tablespace 系统 / 用户 / Undo 系统 / 用户 系统 / file-per-table pg_default / pg_global
行内最大长度 块大小限制(4K) 8K(行不能跨页) ½ 页(约 8K,可溢出) 2GB(TOAST 拆分)

2.3 Oracle 的 Block 结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
一个 8KB Oracle Block 内部:

  ┌─────────────────────────────┐
  │   Block Header(块头)       │  ~100 字节
  │   - 类型(data/index/undo)  │
  │   - SCN、事务槽 (ITL)       │
  ├─────────────────────────────┤
  │   Row Directory(行目录)    │  每行 2~4 字节偏移
  │   指向下面的行实际位置         │
  ├─────────────────────────────┤
  │   Free Space(空闲区)        │  从中间向两边生长
  ├─────────────────────────────┤
  │   Row Data(行数据)          │  实际行 + 列值
  └─────────────────────────────┘

关键:ITL(Interested Transaction List)
  - 每个被修改的 block 记录涉及的事务
  - 是 Oracle MVCC 实现的关键(详见第 5 篇)

2.4 MySQL InnoDB 的 Page 结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
一个 16KB InnoDB Page 内部:

  ┌─────────────────────────────┐
  │  File Header(38B)          │
  │  - 页号、表空间、前后页指针    │
  ├─────────────────────────────┤
  │  Page Header(56B)          │
  │  - 索引 ID、记录数、空闲指针   │
  ├─────────────────────────────┤
  │  Infimum + Supremum Records │  系统伪行(最小/最大)
  ├─────────────────────────────┤
  │  User Records                │  实际行记录
  │  (从下往上增长)              │
  ├─────────────────────────────┤
  │  Free Space                  │
  ├─────────────────────────────┤
  │  Page Directory(槽位)       │  二分查找索引
  ├─────────────────────────────┤
  │  File Trailer(8B)          │  校验和、LSN
  └─────────────────────────────┘

每行结构(COMPACT 格式):
  [变长字段长度][NULL 位图][记录头][事务ID][回滚指针][列1][列2]...
                                   ↑          ↑
                              DB_TRX_ID   DB_ROLL_PTR
                              (用于 MVCC,详见第 5 篇)

2.5 PostgreSQL 的 Page 结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
一个 8KB PostgreSQL Page:

  ┌─────────────────────────────┐
  │  Page Header(24B)          │
  │  - LSN、校验和、行数          │
  ├─────────────────────────────┤
  │  ItemId Array(行指针)       │  4B/项
  ├─────────────────────────────┤
  │  Free Space                  │
  ├─────────────────────────────┤
  │  Tuples(实际行)             │
  │  (从下往上生长)              │
  └─────────────────────────────┘

每个 Tuple 头(23B 起):
  [xmin][xmax][cid][ctid][infomask][...][columns]
   ↑     ↑                ↑
  创建  删除/更新         当前物理位置(用于 HOT 更新)
  事务  事务
1
2
3
4
5
观察:
  - 三家结构非常相似:Header + Directory + Free + Data
  - InnoDB / PG 都把事务信息(MVCC)直接放在行头
  - Oracle 用独立的 ITL 槽(块级别)记录事务
  - PG 8KB 是写死的(编译时定),其他三家可配

2.6 临时表的存储

数据库 临时对象存放 特点
Oracle Temporary Tablespace 每会话独立段,自动回收
SQL Server TempDB 系统级共享库,承载所有临时对象 + 排序
MySQL 临时表空间(ibtmp1) 全局共享 + 会话临时表空间(8.0+)
PostgreSQL pg_default 或专用表空间 默认在 base/ 目录
1
2
3
4
关键差异:
  - SQL Server 用 TempDB 统一承载,是性能调优重点
  - MySQL 8.0 之前临时表都在 ibdata1,导致系统表空间膨胀
  - PG 临时表和普通表共用存储,但通过 relpersistence 区分

三、堆表 vs 索引组织表

这是四大数据库最本质的差异之一:一张表的数据,是按"主键有序"存放,还是无序堆放?

3.1 两种组织方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
堆表(HEAP):
  数据行无序堆放在数据页中
  主键只是"另一个二级索引"
  索引指向行的物理位置(rowid / ctid / RID)

  ┌────────┐         ┌──────────────────┐
  │ 主键索引 │ ──────→│ Heap Page         │
  │ B+树    │         │ [row3][row1][row2]│  ← 无序
  └────────┘         └──────────────────┘

  特点:
  - 主键索引和数据分离
  - 主键查询要"两次 B+ 树查找"(先找索引,再读数据页)
  - 二级索引可以指向 rowid(短)
  - 代表:PostgreSQL、Oracle(默认)、SQL Server(默认)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
索引组织表(IOT / Clustered Table):
  数据行直接按主键有序存放在 B+ 树的叶子节点
  "主键索引就是数据本身"

       主键 B+ 树(叶子就是数据)
  ┌─────────────────────────────┐
  │ [PK=1, row1][PK=2, row2]... │  ← 主键有序
  └─────────────────────────────┘

  特点:
  - 没有独立的"堆",整张表就是一棵 B+ 树
  - 主键查询只需走一棵树
  - 二级索引指向主键值(长,且需要二次查找)
  - 代表:MySQL InnoDB、SQL Server(Clustered)、Oracle IOT

3.2 四家的实际选择

数据库 默认组织方式 可选 IOT?
Oracle 堆表(默认) ✅ IOT 显式声明
SQL Server 堆表(无主键时)/ Clustered(有主键时) ✅ Clustered Index
MySQL InnoDB 强制 IOT ❌ 没有"堆"概念
PostgreSQL 堆表(唯一选项,12+ 抽象了 TAM 接口)

3.3 深入 MySQL InnoDB:为什么主键即数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
InnoDB 中:
  - 每张表 = 一棵按主键排序的 B+ 树(Clustered Index)
  - 这棵树的叶子节点直接存"完整行"
  - 主键之外的索引叫 Secondary Index,叶子节点存的是"主键值"

  表 accounts(id PK, name, balance):

  Clustered Index (按 id):
                       [10│20]               ← 内部节点
                      /       \
              [1│2│...10]   [11│...│20]      ← 叶子 = 整行数据
              ↓               ↓
              id=1 的完整行   id=11 的完整行

  Secondary Index (name):
              [Alice│Bob]                ← 内部节点
              /         \
        [Alice→1][Bob→2]   [Carol→3]     ← 叶子 = name + 主键值
                                       (查到主键后,回 Clustered 树再查一次)
1
2
3
4
5
6
-- 一次查询,两种索引路径差异
SELECT balance FROM accounts WHERE id = 1;
-- 直接走 Clustered 树,1 次 I/O

SELECT balance FROM accounts WHERE name = 'Alice';
-- 走 Secondary 树找到主键 id=1 → 回 Clustered 树找到 balance → 2 次 I/O("回表")

3.4 深入 PostgreSQL:堆表 + ctid

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
PostgreSQL 中:
  - 表数据存在 Heap(堆)中,无序
  - 所有索引(包括主键)都是"二级索引"
  - 索引指向 ctid(物理位置:页号 + 行偏移)

  Heap File:
  Page 0:  [row(id=1)][row(id=3)][row(id=2)]   ← 任意顺序
  Page 1:  [row(id=5)][row(id=4)][row(id=6)]

  Primary Key Index (id):
              [2│4│6]
              /   |   \
      [1→(0,1)] [2→(0,3)] [3→(0,2)]
                  叶子 = id + ctid
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- 看看实际的 ctid
SELECT ctid, id, name FROM accounts;
-- ctid   id  name
-- (0,1)  1   Alice
-- (0,2)  3   Carol
-- (0,3)  2   Bob
-- 物理顺序 ≠ 主键顺序

-- 主键查询也要两步:索引找 ctid → heap 取数据
SELECT balance FROM accounts WHERE id = 1;
-- 走 PK 索引找到 ctid=(0,1) → 回 heap 读 → 2 次 I/O

3.5 两种模型的性能权衡

维度 IOT(InnoDB/MSSQL Clustered) HEAP(PG/Oracle 默认)
主键查询 (一棵树) 慢(两次查找)
二级索引查询 慢(需要回主键树) (直接到 heap)
范围查询(按主键) (叶子有序连续) 慢(需要排序)
插入 主键有序时慢(B+ 树页分裂) (追加到堆末尾)
二级索引大小 大(存主键值) 小(存 ctid/rowid)
二级索引重建(改主键时) 极慢(所有二级索引都要改) 快(指向物理位置,不变)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
实战含义:
  - InnoDB 中改主键 = 灾难(所有二级索引失效重建)
    → 所以 InnoDB 主键推荐:自增 BIGINT,永不变
  - PG 中改主键 = 改个约束(索引重建,但 heap 不动)
    → PG 改主键代价相对小

  - InnoDB 二级索引查询性能 < PG(需要"回主键树")
  - 但 InnoDB 主键范围查询(按时间、按 ID 范围)> PG

这就是为什么 MySQL 表设计强调"主键设计",PG 强调"索引设计"。

四、缓冲池

修改的是内存中的页,不是磁盘上的页。这块内存就叫"缓冲池"(Buffer Pool)。

4.1 缓冲池的作用

1
2
3
4
5
6
7
8
没有缓冲池:
  读:每次 SELECT 都从磁盘读页 → 极慢
  写:每次 UPDATE 都直接刷盘 → 极慢 + 磁盘磨损

有缓冲池:
  读:先查 Buffer Pool,命中直接返回;未命中则从磁盘读入
  写:直接改 Buffer Pool 中的页(变成"脏页" dirty page)
       后台异步刷盘(详见第 7 篇 Checkpoint)

4.2 替换算法:LRU 与冷热分离

朴素 LRU 的问题:一次全表扫描会把热点页全冲掉(“缓存污染”)。所以四大数据库都对 LRU 做了改进。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
改进版 LRU(中点插入策略):
  把 LRU 链分成两段:

  [Old sublist(冷区)] | [New sublist(热区)]
              ↑                   ↑
           midpoint           midpoint move threshold
       新读入的页插这里
       (而不是直接进热区)

  真正热的数据:在冷区停留超过 innodb_old_blocks_time(默认 1 秒)
                才会被推入热区
  → 全表扫描的页只会短暂留在冷区,被快速淘汰,不会冲掉热区

4.3 四家的实现

数据库 缓冲池名称 冷热分离 多实例 后台写
Oracle Buffer Cache ✅(KEEP/RECYCLE/DEFAULT 池) ✅(多池) DBWn
SQL Server Buffer Pool ✅(Just-In-Time) 单一池 Checkpoint / Lazy Writer
MySQL InnoDB Buffer Pool ✅(young/old sublist) ✅(多 instance) Page Cleaner Thread
PostgreSQL Shared Buffers ✅(环形缓冲区) 单一池 bgwriter / checkpointer

4.4 关键参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
-- Oracle
ALTER SYSTEM SET db_cache_size = 8G;  -- Buffer Cache 大小
ALTER SYSTEM SET db_keep_cache_size = 1G;  -- 热点小表保留

-- SQL Server
-- "max server memory" 控制整个 Buffer Pool 上限
EXEC sp_configure 'max server memory (MB)', 16384;
RECONFIGURE;

-- MySQL
[mysqld]
innodb_buffer_pool_size = 8G       -- 最重要参数,建议物理内存 60~70%
innodb_buffer_pool_instances = 8   -- 多实例,减少锁竞争
innodb_old_blocks_time = 1000      -- 冷区停留时间(ms)

-- PostgreSQL
shared_buffers = 2GB               -- 推荐 25% 物理内存
effective_cache_size = 8GB         -- 仅给优化器参考,不实际分配
1
2
3
4
5
调优经验:
  - MySQL:innodb_buffer_pool_size 是 OLTP 性能第一参数,通常设为物理内存 70%
  - PostgreSQL:shared_buffers 一般不超过物理内存 25%(PG 还要用 OS page cache)
  - SQL Server:max server memory 通常留 4~8GB 给 OS
  - Oracle:SGA(含 Buffer Cache)+ PGA 总和 ≈ 物理内存 80%

五、列存扩展

行存适合 OLTP,列存适合 OLAP。四大数据库都做了列存扩展来支持分析查询。

5.1 SQL Server Columnstore

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
SQL Server 2012+ 引入 Columnstore Index:
  - 一张表可以有 Columnstore Index(聚集或非聚集)
  - 行组(Rowgroup):每 100 万行为一组,按列编码存储
  - 列段(Column Segment):每个列段存储一组数据
  - 配合 Batch Mode 执行(向量化)

CREATE CLUSTERED COLUMNSTORE INDEX cci ON big_table;
-- 整表变成列存

CREATE NONCLUSTERED COLUMNSTORE INDEX ncci ON big_table(col1, col2);
-- 局部列存(2014+)

5.2 Oracle In-Memory

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Oracle 12.1.0.2+ 引入 In-Memory Column Store(IMCS):
  - 数据在 SGA 中以"行存 + 列存"双格式存储
  - 列存叫 In-Memory Compression Unit(IMCU)
  - OLTP 走行存(Buffer Cache),OLAP 走列存(IMCS)

ALTER TABLE big_table INMEMORY;
-- 该表的列被加载到 IMCS

特点:
  - 行/列双写:内存占用大
  - 透明:优化器自动选择
  - 商业版高级特性

5.3 PostgreSQL 的列存方案

PG 本身没有内置列存,但有方案:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
1. cstore_fdw(早期,Citus 出品)
   - 用 FDW(Foreign Data Wrapper)实现
   - 已基本停更

2. Citus(已并入 PG 生态)
   - 列式压缩 + 分布式

3. TimescaleDB(时序场景)
   - 时序数据自动按时间分区 + 压缩

4. Greenplum(PG 分支)
   - MPP 数据库,原生列存支持

5.4 MySQL 的列存现状

1
2
3
4
MySQL 8.x 没有列存。
- HeatWave(MySQL Cloud 在 OCI 上的扩展)有列存加速
- 但开源 MySQL 没有列存
- 分析查询需要走 OLAP 引擎(如外接 Doris / ClickHouse)

5.5 行存 vs 列存 选型

场景 推荐
高并发 OLTP(按主键点查 / 范围更新) 行存(默认)
多维聚合分析(GROUP BY 多列、SUM/AVG) 列存
混合负载(HTAP) 行存为主 + 列存索引 / IMCS
大宽表 + 高压缩比 列存

六、内置存储引擎矩阵

6.1 MySQL:可插拔引擎之王

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
SHOW ENGINES;

-- 主要引擎:
-- InnoDB     默认,事务、行锁、MVCC
-- MyISAM     老引擎,表锁,崩溃恢复弱
-- Memory     内存表,重启丢失
-- Archive    高压缩归档,只支持插入
-- NDB        分布式集群(MySQL Cluster)
-- Blackhole  什么都不存(用于复制中转)
-- 第三方:RocksDB(MyRocks)、TokuDB、TiDB
1
2
3
4
设计:
  每张表可以选不同引擎
  CREATE TABLE orders (...) ENGINE=InnoDB;
  CREATE TABLE logs (...) ENGINE=Archive;

6.2 Oracle:段类型 + 表压缩

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Oracle 不是"多引擎",而是统一引擎 + 多种段类型:

  段类型:
  - Heap Table(默认堆表)
  - Index-Organized Table(IOT,索引组织表)
  - Partitioned Table(分区表)
  - Cluster Table(聚簇表)
  - External Table(外部表,HDFS 文件等)

  压缩选项:
  - OLTP Table Compression
  - Advanced Compression
  - Hybrid Columnar Compression(HCC,Exadata 专属)

6.3 SQL Server:堆表 vs Clustered

1
2
3
4
5
6
7
8
9
SQL Server 一张表的状态:
  - 堆表(Heap):没有 Clustered Index
  - Clustered Table:有 Clustered Index(按主键组织)

  CREATE TABLE t (id INT PRIMARY KEY, ...);
  -- PRIMARY KEY 隐式创建 Clustered Index → 表变成 Clustered

  ALTER TABLE t DROP CONSTRAINT pk_t;
  -- 删主键后,表变回 Heap
1
2
3
4
内存优化表(In-Memory OLTP):
  - SQL Server 2014+ 引入
  - 完全在内存中,无锁(用 MVCC)
  - 用于极高性能 OLTP

6.4 PostgreSQL:Table Access Method 抽象

1
2
3
4
5
6
7
8
PG 12 引入 Table Access Method (TAM) 抽象:
  - 12 之前:所有表都是 Heap
  - 12+:可以插件化实现新表类型
  - 目前主流还是 heap TAM

  扩展:
  - zheap(PG 社区研发,目标减少表膨胀,未发布)
  - 其他自定义 TAM

七、性能影响

7.1 为什么 OLTP 必须行存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
OLTP 工作负载特征:
  - 大量短事务
  - 按主键 / 索引点查
  - 修改单行多列(UPDATE)

OLAP 工作负载特征:
  - 少量长查询
  - 大范围扫描
  - 按列聚合(GROUP BY + SUM/AVG)

行存优势(OLTP):
  - 一次 I/O 读出整行(修改单行多列只需 1 次页 I/O)
  - 局部性好(同时访问同一行的多列)
  - 事务实现简单(锁单行)

7.2 表设计原则

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
InnoDB 表设计:
  1. 必须有主键(推荐 BIGINT AUTO_INCREMENT)
     → 没有 PK 的话会选唯一键,再没有就生成隐藏 6 字节 ROW_ID
  2. 主键尽量短(让二级索引小)
  3. 主键不要变更(否则所有二级索引重建)
  4. 主键单调递增(减少 B+ 树页分裂)

PostgreSQL 表设计:
  1. 主键选 UUID 或 SERIAL/BIGSERIAL 都行(不影响 heap)
  2. 关键在索引设计
  3. 注意 fillfactor(默认 100,频繁更新降到 80~90 留 HOT 空间)

SQL Server 表设计:
  1. 选择 Clustered Index 列(默认主键)
  2. Clustered 列要短、单调、不变
  3. 大表考虑分区 + Columnstore 索引(混合)

Oracle 表设计:
  1. 默认 Heap Table,分析场景考虑 IOT 或分区表
  2. 用 Sequence 而非 AUTO(Oracle 12c+ 才有 IDENTITY)
  3. HCC 压缩用于 Exadata 上的数仓

7.3 常见性能陷阱

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
1. MySQL InnoDB:UUID 主键
   问题:UUID 无序 → B+ 树频繁页分裂 → 插入性能差 + 索引膨胀
   方案:用 BIGINT AUTO_INCREMENT 或有序 UUID

2. PostgreSQL:高更新场景
   问题:MVCC 写新 tuple + 旧 tuple 不删 → 表膨胀
   方案:autovacuum 配置、fillfactor 降低、pg_repack 重整

3. SQL Server:Clustered Index 选错
   问题:用 GUID 当 Clustered Key → 页分裂 + 缓存碎片
   方案:Clustered 用 IDENTITY INT/BIGINT,GUID 用 ROWGUIDCOL + NonClustered

4. Oracle:HCC 压缩误用
   问题:在 OLTP 表上用 HCC → 锁升级 + 性能崩溃
   方案:HCC 仅用于只读 / 批量加载场景

八、小结

本文学习了存储引擎的核心原理:

  • 物理存储层级:Tablespace → Segment → Extent → Page → Row
  • 四家的页大小:Oracle 8K / MSSQL 8K / InnoDB 16K / PG 8K
  • 行存 vs 列存:OLTP 用行存,OLAP 用列存
  • 堆表 vs 索引组织表(IOT):本质差异
    • InnoDB 强制 IOT,主键就是数据
    • PG/Oracle 默认堆表,所有索引都是二级
    • SQL Server 由是否 Clustered 决定
  • 缓冲池:LRU 冷热分离避免缓存污染
  • 列存扩展:MSSQL Columnstore / Oracle IMCS / PG 第三方
  • 各家内置存储引擎矩阵
1
2
3
4
记住三句话:
  1. 物理存储 = 页为单位的 B+ 树 / 堆文件
  2. InnoDB 主键就是数据,PG 主键只是另一个索引
  3. 修改是先改内存页,后台异步刷盘(这就是"事务持久性"实现的基础)

下一篇将深入索引原理:为什么几乎所有索引都是 B+ 树?PG 的 GIN/GiST 是什么?为什么联合索引要最左前缀?