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 秒实验
- 把
rows从一百万改成一百亿,重跑。堆表全扫变 2.5 亿块,IOT 还是 3 块。 数据量越大,两者的差距越大——这就是为什么大表必须走索引。 - 把
ENAME的字节数从 6 改成 200(假设存满VARCHAR2(200)),重跑。 一行从 31 字节涨到 225 字节。 如果这张表有一亿行,光这一列就多占 19GB。 - 把
per_blk从 40 改成 80(行变小、每块装更多),重跑。块数减半,全扫代价也减半。 这说明「行小一点」不只是省空间,还直接省 I/O。
常见误区
- 以为「不加
order by也是按插入顺序返回」。 堆表不保证。 靠默认顺序写代码,换一次重建表就可能全乱。 - 以为临时表「表也是临时的」。 表的定义永久,只有数据临时。
USER_TABLES里一直能看到它。 - 按声明长度估容量。
VARCHAR2(200)实际只存实际长度,按 200 算会高估两三倍。 - 以为「IOT 更快」。 只对主键查快,按别的列查照样全扫。
自测三问(点开对答案)
1. 堆表为什么不保证返回顺序? —— **因为数据按插入顺序堆着,删除的空位会被复用。** **所以物理顺序和逻辑顺序无关**——要顺序必须写 `order by`。 2. 一行里那 1 字节的「长度前缀」是干什么的? —— **告诉 Oracle 这一列实际占多少字节。** **变长类型必须靠它切分**,定长类型可以省掉。 3. IOT 适合什么表? —— **短行 + 按主键查的表**,比如码表、配置表。**长行会溢出,按别的列查照样全扫。** 4. 临时表的两种作用域有什么区别? —— **`ON COMMIT DELETE ROWS` 是事务级(提交就清),** **`ON COMMIT PRESERVE ROWS` 是会话级(断开才清)。** **默认是前者**,这是很多人「提交后数据没了」的原因。下一篇讲创建表:除了列定义,还有一堆物理属性可调——PCTFREE、LOGGING、压缩、IOT。
- ☐ (本篇完,下一篇 9.2 创建数据表)
相关笔记
- 创建数据表
- 表空间与数据文件的关系
- Oracle 的默认表空间
-
- 🐬 MySQL必知必会/00-索引
- 🏺 Oracle从入门到精通 · 索引