# 第 9 章 存储与物理结构 - 配套代码

本章脚本带你「亲眼看到」PostgreSQL 在硬盘上的样子：从 PGDATA 目录、表的 OID/relfilenode、8KB 页内布局，到 TOAST 切片、HOT 更新比例。

## 准备工作

1. 跑 `psql -h 127.0.0.1 -U postgres -d learn_pg -f ../init.sql` 初始化测试表（带 `ch9_` 前缀）
2. 安装依赖：`pip install "psycopg[binary]>=3.1"`
3. （可选）通过环境变量覆盖默认连接：`export PG_DSN="host=... port=... dbname=... user=..."`

> 多个脚本依赖 `pageinspect`、`pgstattuple`、`pg_visibility` 三个扩展，已经在 `init.sql` 中 `CREATE EXTENSION IF NOT EXISTS` 了，**需要 superuser 权限**。

## 脚本一览（推荐运行顺序）

| 脚本 | 一句话说明 | 关键 PG 特性 |
|------|------------|--------------|
| `01_pgdata_explorer.py` | 把表名翻译成磁盘文件路径，演示 OID vs relfilenode | `pg_relation_filepath` / `VACUUM FULL` 后 relfilenode 变更 |
| `02_page_inspect.py` | 用 `pageinspect` 看 8KB 页头、行指针、HeapTuple 头 | `page_header` / `heap_page_items` / HOT 链 |
| `03_toast_demo.py` | 插入超大字段触发 TOAST，比较 4 种 STORAGE 策略 | TOAST chunk / `pg_toast.pg_toast_<oid>` / 压缩算法 |
| `04_fillfactor_hot.py` | 对比 fillfactor=100 / 70 时 HOT 更新比例 | `n_tup_hot_upd` / 物理体积 |

运行示例：

```bash
python 01_pgdata_explorer.py
python 02_page_inspect.py
python 03_toast_demo.py
python 04_fillfactor_hot.py
```

## 预期输出

`01_pgdata_explorer.py` 会列出 `ch9_demo_orders` 在磁盘上的相对路径（形如 `base/16384/16400`），并演示 `VACUUM FULL` 后 relfilenode 的变化：

```
VACUUM FULL 之前 relfilenode = 16400
VACUUM FULL 之后 relfilenode = 16453
oid 不变，但物理文件被重写：16400 → 16453
```

`04_fillfactor_hot.py` 会打印类似：

```
  table         fillfactor   updates   hot_upd     hot %    dead size(KB)
  ──────────────────────────────────────────────────────────────────────
  ch9_hot_full         100     50000     12345     24.7%    37655      640
  ch9_hot_70            70     50000     49123     98.2%      877      192
```

## 常见报错

- `connection refused` → PostgreSQL 服务未启动，检查 `pg_isready`
- `relation "ch9_demo_orders" does not exist` → 没跑 `init.sql`，先 `psql ... -f ../init.sql`
- `permission denied for extension pageinspect` → 需要 superuser；用 `postgres` 用户跑 `init.sql`
- `function get_raw_page(...) does not exist` → 缺少 `pageinspect` 扩展，`CREATE EXTENSION pageinspect;`
- `out of memory` 或 chunk 数为 0 → TOAST 行内压缩成功，本就不会写 chunk，属正常
