Oracle 把「数据存在哪」分成四级:块 → 区 → 段 → 表空间。 这四级全是逻辑概念,盘上找不到叫「块」的文件。 块是「一次搬多少」,区是「一次加多少」,段是「一个对象占多少」,表空间是「归谁管」。
上一篇把库拆成「实例+文件」,这一篇把「文件里的东西」再拆细。 逻辑结构和物理结构(下一篇)是同一件事的两个视角: 表空间是「名字」,数据文件是「文件」。 这一篇的四个词会在后面反复出现:第 8 章建表空间、第 9 章建表、第 11 章做分区, 每次都要说「这个段放在哪个表空间」。
一、块:I/O 的最小单位
块(block)是 Oracle 读写磁盘的最小单位,默认 8 KB。哪怕你只查一行,它也要读一整块。
所以「块里有几行」直接决定了点查的效率:
一行 100 字节、块 8 KB,一块能放八十行——查一行等于白读了另外七十九行。
块大小建库时定(db_block_size),之后不能改。
这是 1.3 节那种「装完就定死」的参数里最典型的一个。
二、区:分配的最小单位
区(extent)是一组连续的块。 Oracle 分配空间不是一块一块给,而是一个区一个区给。 为什么成区地给?因为连续比零散快:一次给 8 个连续的块,顺序读的时候磁头不用来回跳。 表刚建时只有一个初始区,数据多了就再加一个区。 这也解释了一个常见现象: 删了很多数据,表空间却没变小——删掉的是行, 区还在段里,没还给表空间。第 8 章讲表空间维护时,会碰到这个问题。
三、段:一个对象一个段
段(segment)是「一个数据库对象占的那一堆区」。 一张表一个表段,一个索引一个索引段,回滚有回滚段,排序有临时段。
| 段的类型 | 属于谁 | 干什么 |
|---|---|---|
| 表段 | 表 | 存行数据 |
| 索引段 | 索引 | 存索引条目 |
| 回滚段 | 撤销表空间 | 存「改之前的值」,用于回滚与闪回 |
| 临时段 | 临时表空间 | 排序、临时表用,用完不立刻还 |
「一个对象一个段」这条很重要: 建索引不只是建一棵树,还会多占一个段。 所以索引不是白来的,它在表空间里真的占地方。
四、表空间:逻辑容器
表空间(tablespace)是段的容器,一个库至少有 SYSTEM 和 SYSAUX 两个。 它是逻辑的,不直接对应文件。 一个表空间可以摊到多个数据文件上: 一个文件写满了,再加一个文件进去,表空间就变大了。 但一个数据文件只能属于一个表空间。 这层抽象换来两件事: 一是「扩容」不用动表(加个文件就行); 二是「配额」可以按表空间算(这个用户在这个表空间最多用多少)。 第 12 章讲用户配额,用的就是这个单位。
五、动手 Demo:量一量:一页到底能放几行
「块」这个词很抽象,但可以量。
SQLite 的页就是块,PRAGMA page_size 给字节数,PRAGMA page_count 给页数。
这段代码插入 100/500/1000/2000 行,每次都重新算页数,再除一下——「每页几行」就出来了。
# 块:SQLite 里叫 page,默认 4096 字节
f.execute('PRAGMA page_size').fetchone() # 4096
f.execute('PRAGMA page_count').fetchone() # 空表时 2 页
for n in (100, 500, 1000, 2000):
f.execute('DELETE FROM t')
rows = [(i, 'name%05d' % i, 'X' * 60) for i in range(1, n + 1)]
f.executemany('INSERT INTO t VALUES (?,?,?)', rows)
f.commit()
pc = f.execute('PRAGMA page_count').fetchone()[0]
print(n, pc, n / float(pc)) # 行数、页数、每页几行
# 表空间:SQLite 用 ATTACH 挂第二个库文件
f.execute('ATTACH DATABASE ? AS side', ('side.db',))
f.execute('select * from side.dept').fetchall()
f.execute('pragma database_list').fetchall()
实跑输出:
--------------------------------------------------------------------
第七课:逻辑存储结构——块、区、段、表空间
--------------------------------------------------------------------
**Oracle 把「存哪儿」分成四级:**
**块(block)→ 区(extent)→ 段(segment)→ 表空间(tablespace)。**
**这四级是逻辑的,不是目录结构。**
**盘上你看不到「块」这个文件,它只是磁盘读写的最小单位。**
四级各是什么
--------------------------------------------------------------------
SQL> select lv, unit, size, note from lvl
LV UNIT SIZE NOTE
----------------------------------------------------------------------------------
1 块 数据块 block 默认 8 KB **读写的最小单位**,一次 I/O 至少搬一块
2 区 区 extent 若干连续块 **分配的最小单位**,段不够就加一个区
3 段 段 segment 若干区 **一个对象一个段**:表、索引、回滚各有各的段
4 表空间 表空间 tablespace 若干段 **逻辑容器**,对应一到多个数据文件
已选择 4 行
**记法:**
**块是「一次搬多少」,区是「一次加多少」,段是「一个对象占多少」,表空间是「归哪个部门管」。**
在 SQLite 上量一量「块」和「行」的关系
--------------------------------------------------------------------
`PRAGMA page_size` → **4096 字节**(这就是 SQLite 的「块」)
建完空表:`page_count` = **2**
**100 行**(每行约 77 字节)→ 页数 **4**,每页约 **25.0 行**
**500 行**(每行约 77 字节)→ 页数 **12**,每页约 **41.7 行**
**1000 行**(每行约 77 字节)→ 页数 **22**,每页约 **45.5 行**
**2000 行**(每行约 77 字节)→ 页数 **42**,每页约 **47.6 行**
为什么块大小是个权衡
--------------------------------------------------------------------
**块小**:一次 I/O 搬得少,**点查(按主键取一行)快**;
但同样多的数据要更多次 I/O,**全表扫描慢**。
**块大**:顺序读快、**全表扫描和范围扫描占便宜**;
但点查会把一堆用不上的行也读进来,**内存里塞的都是废物**。
**Oracle 默认 8 KB,就是这个权衡的中间点。**
**建库时 `db_block_size` 定下就不能改**(要重建库)——
**所以它属于 1.3 节那种「装完就定死」的参数。**
一个表空间可以有多个数据文件
--------------------------------------------------------------------
**这是逻辑与物理的分界点:**
**表空间是「名字」,数据文件是「盘上的文件」。**
**一个表空间可以摊到好几个文件上(一个文件写满了再加一个),但一个数据文件只属于一个表空间。**
**SQLite 没有表空间,但它有 ATTACH:**
**把另一个库文件挂到同一个连接上,用别名访问。**
**这就是「一个实例管多个文件」的最简版。**
SQL> select * from side.dept
DEPTNO DNAME
-----------------
10 ACCOUNTING
已选择 1 行
SQL> pragma database_list
SEQ NAME FILE
-----------------------------------------------------------------
0 main C:\Users\admin\AppData\Local\Temp\ora47_srexe6nz\blk.db
2 side C:\Users\admin\AppData\Local\Temp\ora47_srexe6nz\side.db
已选择 2 行
**`pragma database_list` 列出这个连接上挂着几个库文件**——
**`main` 是主库,`side` 是刚挂上的。** **Oracle 里对应的概念就是「一个表空间配几个数据文件」。**
页大小 4096 字节(SQLite 默认),Oracle 默认是 8192。
这个数决定了一次 I/O 搬多少,两边概念一样、数值不同。
行数从 100 加到 2000,每页行数从 25 爬到 47.6,然后基本稳定。
为什么不是常数?
因为页里有页头、有目录槽,行本身也有开销——小表时这些固定开销占比大,行多了才摊薄。
注意 2000 行时每页 47.6 行,一页 4096 字节,平均每行只占 86 字节,
而实际数据是 77 字节——差的那些就是页头和槽位。
Oracle 的块也一样,块头、行目录、空闲空间都占地方。
ATTACH 那段是「一个实例管多个库文件」的最简版。
Oracle 里对应的是「一个表空间配几个数据文件」——同样是「一个名字,多个文件」。
30 秒实验
- 把
X * 60改成X * 300(每行变长),再跑一遍四组行数。 每页行数会掉到个位数——块大小不变,行变长,装的行就少。 - 给表加一个索引,再看
page_count。页数会涨——索引自己也有段,也要占页。 - 把
page_size调到 8192(建库时设PRAGMA page_size=8192再建表),看同样 2000 行要几页。 这就是 Oracle 的 8 KB 块。
常见误区
- 以为「表空间」是磁盘上的一个文件夹。 它是逻辑容器,真正的文件是数据文件, 一个表空间可以对应好几个文件。
- 以为块越大越快。 块大顺序读快,但点查会把用不上的行一起读进来。 块大小是权衡,不是越大越好,而且建库后改不了。
- 以为删数据能腾出空间。 删的是行,区还在段里;要让表空间真的变小,得 shrink 或重建。
自测三问(点开对答案)
1. 块、区、段、表空间,哪个是「分配」的单位? —— **区。** 块是「读写」的单位,区是「分配」的单位,段是「一个对象占的全部区」, 表空间是「段的容器」。 2. 一个表空间能有几个数据文件?一个数据文件能属于几个表空间? —— **表空间可以有多个数据文件;一个数据文件只能属于一个表空间。**单向的一对多。 3. 为什么删了一半数据,表空间还是那么大? —— **因为删的是行,区没还回去。** 段仍占着那些区,空闲块留在段内部等复用。 **要真变小得 shrink 或重建表。**下一篇换视角:物理存储结构——盘上那五类文件各是什么、丢一个会怎样。 那里会拿 SQLite 的三个文件(主库、WAL、共享索引)对照着看。
- ☐ (本篇完,下一篇 2.3 物理存储结构)