🏺 第2章 Oracle 体系结构
📄 一句话概括

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 秒实验

  1. 把 X * 60 改成 X * 300(每行变长),再跑一遍四组行数。 每页行数会掉到个位数——块大小不变,行变长,装的行就少。
  2. 给表加一个索引,再看 page_count。页数会涨——索引自己也有段,也要占页。
  3. 把 page_size 调到 8192(建库时设 PRAGMA page_size=8192 再建表),看同样 2000 行要几页。 这就是 Oracle 的 8 KB 块。

常见误区

  1. 以为「表空间」是磁盘上的一个文件夹。 它是逻辑容器,真正的文件是数据文件, 一个表空间可以对应好几个文件。
  2. 以为块越大越快。 块大顺序读快,但点查会把用不上的行一起读进来。 块大小是权衡,不是越大越好,而且建库后改不了。
  3. 以为删数据能腾出空间。 删的是行,区还在段里;要让表空间真的变小,得 shrink 或重建。
自测三问(点开对答案) 1. 块、区、段、表空间,哪个是「分配」的单位? —— **区。** 块是「读写」的单位,区是「分配」的单位,段是「一个对象占的全部区」, 表空间是「段的容器」。 2. 一个表空间能有几个数据文件?一个数据文件能属于几个表空间? —— **表空间可以有多个数据文件;一个数据文件只能属于一个表空间。**单向的一对多。 3. 为什么删了一半数据,表空间还是那么大? —— **因为删的是行,区没还回去。** 段仍占着那些区,空闲块留在段内部等复用。 **要真变小得 shrink 或重建表。**

下一篇换视角:物理存储结构——盘上那五类文件各是什么、丢一个会怎样。 那里会拿 SQLite 的三个文件(主库、WAL、共享索引)对照着看。

相关笔记