Oracle 表、块、行与 rowid:一次从堆组织到直接路径读取的漫游
- 堆表:最熟悉的陌生人
- 堆(Heap) 的字面意思是“随便放”。 Oracle 把新来的行扔进第一个能塞下它的块,不维护任何插入顺序。
- 带来的后果:
SELECT * FROM t
出来的顺序可能和插入顺序完全不同。- 大量并发插入时,ASSM(Automatic Segment Space Management) 会把插入打散到不同块,避免“热点块”。
- 块、区、段:一行数据的物理“户籍”
- 块(Block):最小 I/O 单元,常见 8 KB。
- 区(Extent):逻辑上连续的块集合,磁盘上不一定连续。
- 段(Segment):一个对象(表、索引、LOB…)占用的所有区的总和。
示意图(逻辑 → 物理):
Segment
├── Extent #1 (块 1000-1007)
├── Extent #2 (块 3000-3015)
└── ...
- rowid:10 字节的 GPS 坐标
rowid 格式(以 AAAPecAAFAAAABSAAA
为例):
数据对象号 AAAPec
文件号 AAF
块号 AAAABS
行号 AAA ← 行目录条目索引
- 索引叶块里存的就是
(索引键, rowid)
,因此索引读可以做到单块 I/O 直接定位。 - 行在块内移动时,只改行目录指针,rowid 不变(行链接)。
- 行迁移 vs. 行链接:什么时候多走一段路
场景 | 定义 | 对索引读 | 对全表扫描 |
---|---|---|---|
行迁移 Row Migration | 行太长或更新后变大,原块放不下,整行搬到新块,原块留一个“转发地址”。 | 先到原块,再按转发地址到新块,额外一次逻辑/物理 I/O。 | 全表扫描本来就把段里所有块都扫一遍,转发地址被忽略,无额外代价。 |
行链接 Row Chaining | 行本身比块还大,必须拆成多块存储。rowid 不变。 | 读整行时需要把多块拼起来;如果只查的列都在第一块,则不受影响。 | 同索引读。 |
一句话记忆: 迁移 = 搬家后留纸条;链接 = 一家人分房睡。
- 高水位与低高水位:全表扫描的边界
- 高水位(HWM):段中已格式化/使用过的最末块。
- 低高水位(Low HWM):已知全部格式化的最末块。
全表扫描流程:
- 从段头拿到 Low HWM,顺序读所有 ≤ Low HWM 的块(肯定格式化过)。
- 读位图,挑出 Low HWM ~ HWM 之间已格式化的块。
- 跳过 >HWM 的块,它们尚未格式化,读出来也没用。
- 直接路径读取:绕过 SGA 的“绿色通道”
- 常规路径:块 → SGA Buffer Cache → PGA。
- 直接路径读取(Direct Path Read):磁盘 → PGA,完全跳过 Buffer Cache。
触发场景:
- 并行查询
- CTAS / ALTER INDEX REBUILD / ALTER TABLE MOVE
- LOB 读取、临时表空间扫描
优势:
- 减少 Buffer Cache 的闩竞争。
- 支持“多块读”(Multi-block I/O),一次性把大量块读进 PGA。
注意区分:
- 多块读是一个进程一次 I/O 读多个块。
- 并行读是多个进程各读各的块,每个进程可以单块或多块。
把 Oracle 存储层拆开来看,其实就是一部“地址学”:
- 块号告诉你住哪条街;
- rowid 告诉你门牌号;
- 行迁移/链接告诉你可能要多拐几个弯;
- HWM 告诉你别去空街区;
- 直接路径读取则像一条 VIP 通道,让大部队绕过市中心直达目的地。
理解这些概念后,再面对执行计划里的 TABLE ACCESS FULL
, TABLE ACCESS BY INDEX ROWID
, TABLE FETCH CONTINUED ROW
或等待事件里的 direct path read
,就不会只看到冷冰冰的单词,而能脑补出行在磁盘上“真实行走”的路线。