🏺 第9章 数据表对象
📄 一句话概括

Oracle 里的「表」有五类:堆表、索引组织表、分区表、临时表、外部表。 日常写的 CREATE TABLE 建的是堆表,其余四种各有专门用途。 堆表的关键词是「顺序无所谓」——所以「不加 order by 的顺序不确定」不是 bug,是定义。 IOT 反过来,整张表就是按主键排的一棵 B 树。

📝 本章和前面几章的关系

第 8 章讲完「空间怎么分」,这一章讲「表怎么设计」。 表是 Oracle 里最基础的对象——索引、视图、同义词都围着它转。 这一篇是概念篇,没有可执行命令。 但「五种表」和「一行占多少字节」这两件事,是后面估容量、选结构的基础。

一、五种表,先认个脸

类型 怎么存 适合什么
堆表 按插入顺序堆,删除的空位复用 绝大多数业务表
索引组织表 按主键排,表本身就是 B 树 码表、按主键查的小表
分区表 按规则切成若干独立段 大表、历史数据
临时表 数据只在会话或事务内可见 中间结果
外部表 数据在文件里,表只是个壳 导数据、查日志

堆表(heap)的关键词是「无所谓顺序」。 插进去就在哪儿,删掉留下的空位以后会被复用。 所以「没有 order by 的查询,返回顺序不确定」——这不是 Oracle 的毛病,是堆表的定义。 靠「默认顺序」写代码,换一个版本、换一次重建表就可能全乱。 分区表(第 11 章细讲)解决的是「大表难管」: 一张十亿行的表切成按月分区,删一个月的数据就是 DROP PARTITION, 比 DELETE 快几千倍,还不占 undo。 临时表容易被误解成「表也是临时的」。 不是——表的定义是永久的,USER_TABLES 里一直能看到它,只是数据在会话或事务结束后清空。

二、行结构:一行到底占多少字节

一行的物理结构是「行头 + 列数据」。 行头约 3 字节,然后是列数(1 字节)、每列的长度前缀(1 字节)、列值本身。 所以「声明 VARCHAR2(200)」不代表占 200 字节——变长类型只存实际长度, 存 "AB" 就占 2 字节加 1 字节长度前缀。 这一点在估容量时最容易高估,按声明长度算出来的空间,往往比实际大两三倍。 全是定长列时,长度前缀可以省掉——因为每行都一样长,不需要记。 这就是「能用定长就用定长」在超高频写入表上的意义:省的是每行的 1 个字节乘上几亿行。 还有一条硬限制:一行不能跨块。 一个 8K 块,行最长也就 8K 左右(实际可用 8KB 减块头)。 所以 VARCHAR2(4000) 两列就快满了, 真想存大文本得用 CLOB(它把数据存到单独的 LOB 段里,行里只留指针)。

三、找一行的代价:堆表全扫 vs IOT 走树

堆表上没有索引,按非主键列找一行,只能全表扫。 一百万行、每块 40 行,就是两万五千块——全扫完才知道有没有。 IOT 按主键找,走 B 树:三层高度,只读三块。 两万五千块 vs 三块,差三个数量级。 这就是 IOT 存在的理由。 但 IOT 只对「主键查询」快。 按别的列查,它照样全扫——因为整张表是按主键排的,别的列上没有顺序。 所以「IOT 更快」这句话不成立,得说「按主键查更快」。 IOT 还有两个代价: ① 插入要维持顺序,主键乱序插入会频繁页分裂;② 长行会溢出到 OVERFLOW 段,多一次 I/O。 所以它适合「短行 + 主键查」的码表,不适合「长行 + 范围查」的业务表。

四、临时表的两种作用域

选项 作用域 什么时候清空
ON COMMIT DELETE ROWS 事务级 一 COMMIT 就清
ON COMMIT PRESERVE ROWS 会话级 会话断开才清

默认是事务级(DELETE ROWS)。 很多人建了临时表发现「提交之后数据没了」,就是这个默认值在起作用。 临时表的数据放在临时表空间,不写 redo、不写 undo。 所以它「崩溃了就是没了」——这是设计,不是缺陷。千万别把「要恢复的中间结果」放临时表。 临时表还有个容易忽略的点:它也有统计信息,但只在会话内有效。 所以执行计划可能和普通表不一样, 批量处理时觉得「临时表怎么这么慢」,往往是因为没收集统计信息。

五、动手 Demo:算一行占多少字节,再比堆表和 IOT 的查找代价

这一篇没有可执行的 Oracle 命令,全是「算」和「比」。 而算,沙箱里能做得很实。 我先列出五种表各自的定位,再按行结构算出一行占多少字节, 最后算一遍「堆表全扫两万五千块」和「IOT 走三层树读三块」的差距。

# 一、五种表
for kind, fits in (('堆表', '绝大多数业务表'),
                   ('IOT', '码表、按主键查的小表'),
                   ('分区表', '大表、历史数据'),
                   ('临时表', '中间结果'),
                   ('外部表', '查文件、导数据')):
    print('%-6s %s' % (kind, fits))

# 二、一行占多少字节:行头 3 + 列数 1 + 每列(1 长度 + 数据)
cols = [('EMPNO', 4), ('ENAME', 6), ('JOB', 5), ('SAL', 4), ('DEPTNO', 2)]
total = 3 + 1 + sum(n + 1 for _, n in cols)
for name, n in cols:
    print('%-7s 占 %d 字节' % (name, n + 1))
print('一行合计', total, '字节')

# 三、找一行的代价:堆表全扫 vs IOT 走 B 树
rows, per_blk = 1000000, 40
blocks = rows // per_blk
print('一百万行共 %d 块' % blocks)
print('堆表无索引 -> 最坏扫 %d 块' % blocks)
print('IOT 按主键 -> 3 块,差 %d 倍' % (blocks // 3))

实跑输出:

--------------------------------------------------------------------
第三十九课:数据表概述——五种表,一种比一种专
--------------------------------------------------------------------

  **Oracle 里「表」不是一个东西,是五种东西。**
  **日常写的 `CREATE TABLE` 建的是「堆表」,但还有四种各有各的用处。**

  五种表,先认个脸
  --------------------------------------------------------------------

  SQL> select kind as 类型, how as 怎么存, fits as 适合 from ttype
  类型              怎么存                         适合
  -----------------------------------------------------------------------------
  堆表(heap)      数据随便放,按插入顺序堆       绝大多数业务表
  索引组织表(IOT) 数据按主键顺序排,本身就是索引 码表、配置表、按主键查的小表
  分区表            按规则切成若干段               大表、历史数据、要按时间清理
  临时表            数据只在会话或事务内可见       中间结果、批量处理
  外部表            数据在文件里,表只是个壳       导数据、查日志文件
  已选择 5 行
  **堆表的关键词是「无所谓顺序」。**  **插进去就在那儿,删除留下的空位以后会被复用。**
  **所以「没有 `order by` 的查询,返回顺序不确定」——这不是 bug,是定义。**
  **索引组织表(IOT)反过来:整张表就是一棵 B 树,按主键排好。**
  **好处是「按主键查」一次索引扫描就拿到整行;坏处是「插入要维持顺序」,主键乱序插入会频繁页分裂。**

  一行到底占多少字节
  --------------------------------------------------------------------
  **一行的物理结构是「行头 + 列数据」。**  **行头约 3 字节,然后是列数、每列的长度、列值本身。**
  **所以「声明 `VARCHAR2(200)`」不代表占 200 字节——变长类型只存实际长度。**
  **EMPNO   NUMBER(4)      实际占 4 字节(含 1 字节长度前缀)**
  **ENAME   VARCHAR2(10)   实际占 6 字节(含 1 字节长度前缀)**
  **JOB     VARCHAR2(9)    实际占 5 字节(含 1 字节长度前缀)**
  **SAL     NUMBER(7,2)    实际占 4 字节(含 1 字节长度前缀)**
  **DEPTNO  NUMBER(2)      实际占 2 字节(含 1 字节长度前缀)**
  **合计约 30 字节——行头 3 字节、列数 1 字节、每列「1 字节长度 + 数据」。**
  **如果全是定长列,长度前缀可以省掉,那就更省空间。**
  **这也是为什么「能用定长就用定长」在超高频写入的表上是有意义的。**

  堆表找一行要多久,IOT 要多久
  --------------------------------------------------------------------
  **一百万行、每块 40 行,共 25000 块。**
  **堆表上没有索引,按 `ename` 找一行,最坏要扫 25000 块**——  **这就是「全表扫描」。**
  **IOT 按主键找,走 B 树:三层高度,只读 3 块。**  **25000 块 vs 3 块,差 8333 倍。**
  **但 IOT 只对「主键查询」快。**  **按 `ename` 查,它照样得扫全表**——
  **因为整张表就是按主键排的,`ename` 上没有顺序。**
  **所以「IOT 更快」这个说法不成立,得看「按什么查」。**

  临时表:两种作用域
  --------------------------------------------------------------------

  SQL> select kind as 选项, scope as 作用域, note as 说明 from tscope
  选项                      作用域 说明
  -------------------------------------------------
  `ON COMMIT DELETE ROWS`   事务级 提交就清空,默认
  `ON COMMIT PRESERVE ROWS` 会话级 会话断了才清空
  已选择 2 行
  **临时表的定义是永久的(建一次一直在字典里),数据是临时的。**
  **这一点很多人误解成「表也是临时的」**——
  **不是。`USER_TABLES` 里一直能看到它,只是里面没数据。**
  **临时表的数据放在临时表空间,不写 redo。**  **所以它不适合「要恢复的中间结果」**——
  **崩溃了就是没了,这是设计,不是缺陷。**

第一段是五种表的一张速查表,日常九成场景都是第一行「堆表」, 另外四种是「碰到特定问题才想起来」的工具。 第二段算的是「一行 emp 实际占多少」。 五列数据 4+6+5+4+2 = 21 字节,五个长度前缀 5 字节,行头 3 字节,列数 1 字节, 加起来 30 字节。 对照声明长度就明白变长类型省在哪: ENAME VARCHAR2(10) 里存的 SMITH 只占 6 字节, JOB VARCHAR2(9) 里存的 CLERK 只占 5 字节。 所以「一行最多能有多长」和「一行实际占多长」是两个数,算空间要用后一个。 第三段是关键对比:一百万行两万五千块,全扫一遍是两万五千次 I/O;IOT 走 B 树只读三块。 差 8333 倍。 这个数字说明「索引」不是「快一点」,是「换一个量级」。 但要记住反面:IOT 只对主键快。 按 ename 查,它一样得扫两万五千块——因为它没有 ename 上的顺序。 所以「选 IOT 还是堆表」,先问「按什么查」。

30 秒实验

  1. 把 rows 从一百万改成一百亿,重跑。堆表全扫变 2.5 亿块,IOT 还是 3 块。 数据量越大,两者的差距越大——这就是为什么大表必须走索引。
  2. 把 ENAME 的字节数从 6 改成 200(假设存满 VARCHAR2(200)),重跑。 一行从 31 字节涨到 225 字节。 如果这张表有一亿行,光这一列就多占 19GB。
  3. 把 per_blk 从 40 改成 80(行变小、每块装更多),重跑。块数减半,全扫代价也减半。 这说明「行小一点」不只是省空间,还直接省 I/O。

常见误区

  1. 以为「不加 order by 也是按插入顺序返回」。 堆表不保证。 靠默认顺序写代码,换一次重建表就可能全乱。
  2. 以为临时表「表也是临时的」。 表的定义永久,只有数据临时。USER_TABLES 里一直能看到它。
  3. 按声明长度估容量。 VARCHAR2(200) 实际只存实际长度,按 200 算会高估两三倍。
  4. 以为「IOT 更快」。 只对主键查快,按别的列查照样全扫。
自测三问(点开对答案) 1. 堆表为什么不保证返回顺序? —— **因为数据按插入顺序堆着,删除的空位会被复用。** **所以物理顺序和逻辑顺序无关**——要顺序必须写 `order by`。 2. 一行里那 1 字节的「长度前缀」是干什么的? —— **告诉 Oracle 这一列实际占多少字节。** **变长类型必须靠它切分**,定长类型可以省掉。 3. IOT 适合什么表? —— **短行 + 按主键查的表**,比如码表、配置表。**长行会溢出,按别的列查照样全扫。** 4. 临时表的两种作用域有什么区别? —— **`ON COMMIT DELETE ROWS` 是事务级(提交就清),** **`ON COMMIT PRESERVE ROWS` 是会话级(断开才清)。** **默认是前者**,这是很多人「提交后数据没了」的原因。

下一篇讲创建表:除了列定义,还有一堆物理属性可调——PCTFREE、LOGGING、压缩、IOT。

相关笔记