Oracle 数据块结构与 PCTFREE / PCTUSED 详解

Oracle 数据块结构与 PCTFREE / PCTUSED 详解

适用版本:Oracle Database 11g / 12c / 19c / 23ai 阅读基础:了解表空间、段、区的基本概念 文档版本:v1.0 / 2026-07


目录


1. 概述:数据块是 I/O 的最小单位

Oracle 数据库中最小的逻辑存储单位是数据块(Data Block),也是 Oracle 读写数据时最小的 I/O 单位[1]。

关键概念

  • 数据块大小:由 db_block_size 参数决定(标准 8K,可选 2K/4K/8K/16K/32K)
  • 数据块是逻辑单位:对应 OS 层多个 OS Block(如 8K 数据块 = 16 个 512B OS Block)
  • 数据块属于段:每个段(如表段、索引段)由若干数据块组成

块大小与多块大小表空间

-- 查看数据库块大小
SHOW PARAMETER db_block_size;

-- 创建非标准块大小的表空间
ALTER SYSTEM SET db_16k_cache_size=256M SCOPE=BOTH;

CREATE TABLESPACE ts_16k
  DATAFILE '/u01/oradata/orcl/ts16k_01.dbf' SIZE 1G
  BLOCKSIZE 16K;

多块读参数

-- 单次多块读最多读多少个块(受 OS 限制)
SHOW PARAMETER db_file_multiblock_read_count;
-- 默认值通常为 8 或 16
-- 8K * 16 = 128K 单次 I/O 大小

2. Oracle 数据块的结构

Oracle 数据块从结构上分为 5 个区域[1][2]:

+--------------------------------+
|  Block Header (块头)           |  <-- 固定大小(约 100-200 字节)
|  - Block Type                  |
|  - Block Address               |
|  - SCN                         |
|  - RBA                         |
|  - ITL (Interested Transaction List) |
+--------------------------------+
|  Table Directory (表目录)      |  <-- 聚簇表才显著
+--------------------------------+
|  Row Directory (行目录)        |  <-- 每行 2 字节指针
+--------------------------------+
|  Free Space (空闲空间)         |  <-- PCTFREE 控制保留量
|                                |
+--------------------------------+
|  Row Data (行数据)             |  <-- 实际数据
|                                |
|                                |
+--------------------------------+

2.1 块头(Block Header)

块头是每个数据块的开头部分,存储块本身的元数据[2]:

元素说明大小
Block Type块类型(数据/索引/Undo/临时)1 字节
Block Address块地址(RDBA)6 字节
SCN块的最近修改 SCN6 字节
RBARedo Block Address8 字节
ITLInterested Transaction List(事务槽)每个 ~24 字节
Checksum校验和4 字节

ITL(Interested Transaction List):记录当前正在修改该块的事务信息,是实现行锁的基础:

ITL 0x01: XID=0x0001.012.00001234  UB=...
ITL 0x02: XID=0x0002.045.00005678  UB=...

每个 ITL 槽约 24 字节,初始数量由 INITRANS 决定,最大由 MAXTRANS 决定(19c 起 MAXTRANS 固定为 255)。

2.2 表目录(Table Directory)

记录该块中包含哪些表的数据:

  • 普通表:每个数据块只属于一个表,Table Directory 只有一项
  • 聚簇表(Cluster):一个数据块可包含多个表的数据,Table Directory 记录所有相关表

2.3 行目录(Row Directory)

记录块中每行的位置指针:

  • 每行在 Row Directory 中占 2 字节
  • 指向行数据在 Row Data 区域的偏移量
  • 行被 DELETE 后,Row Directory 仍保留槽位(可重用,但不收缩)

2.4 行数据(Row Data)

实际存储行数据的地方:

  • 每行包含:行头(3 字节)+ 列数 + 各列长度 + 各列值
  • 列长度小于 251 字节用 1 字节存储长度,大于等于 251 字节用 3 字节
  • NULL 值不占空间(仅在行尾)

2.5 空闲空间(Free Space)

块中尚未使用的空间,位于 Row Directory 和 Row Data 之间:

  • 新 INSERT 在 Row Data 区域底部向上生长
  • 块头和 Row Directory 从顶部向下生长
  • Free Space 被两侧”夹击”
块头    ──┐
           │  向下生长(INSERT 时分配新行槽)
行目录  ──┤

空闲空间   │

           │  向上生长(INSERT 时写入新行数据)
行数据  ──┘

3. PCTFREE 详解

3.1 PCTFREE 的作用

PCTFREE 参数为每个块预留一定比例的空闲空间,用于未来 UPDATE 操作导致的行增长[1][3]。

默认值PCTFREE = 10(即保留 10% 空间给 UPDATE)

-- 建表时指定
CREATE TABLE employees (
  id     NUMBER,
  name   VARCHAR2(100),
  info   CLOB
) PCTFREE 20 PCTUSED 60;

-- 修改已有表的 PCTFREE
ALTER TABLE employees PCTFREE 15;

3.2 PCTFREE 触发逻辑

当 INSERT 数据让块的空闲空间低于 PCTFREE 阈值时,该块将从**空闲列表(Free List)**中移除,不再接受新 INSERT:

假设 db_block_size=8192,PCTFREE=10:

1. 新块:8000 字节可用(扣除块头)
2. INSERT 数据直到剩余空间 ≤ 800 字节(10%)
3. 该块移出 Free List,不再接受新行
4. 仍可接受 UPDATE(剩余的 800 字节给行增长用)

PCTFREE 触发流程

Free List 中的块:
+-------------------------------+
| Block A: free=2000B (>PCTFREE)| ── 继续接受 INSERT
| Block B: free=850B  (>PCTFREE)| ── 继续接受 INSERT
| Block C: free=700B  (<PCTFREE)| ── 移出 Free List(UPDATE 专用)
+-------------------------------+

3.3 PCTFREE 取值规划

场景推荐 PCTFREE理由
只读表 / 历史表0-5不需要 UPDATE,最大化存储密度
行很少 UPDATE10(默认)平衡空间利用与扩展需求
行经常 UPDATE 增长20-30预留空间避免行迁移
频繁扩展的列(CLOB/BLOB)30-40防止严重行迁移

估算 PCTFREE 公式

PCTFREE = (平均 UPDATE 增长大小) / (块大小 - 块头开销) × 100

例如:块 8K,块头 200B,平均行 UPDATE 增长 800B:

PCTFREE = 800 / (8192 - 200) × 100 ≈ 10%

4. PCTUSED 详解

4.1 PCTUSED 的作用

PCTUSED 参数控制何时将块重新加入 Free List[3]:

  • 当块中已用空间低于 PCTUSED 阈值时,块重新加入 Free List
  • 配合 PCTFREE 形成”上下界”控制

默认值PCTUSED = 40

工作机制

PCTFREE=20  PCTUSED=40

块使用率 100% ───┐

                  │  INSERT(直到 free=PCTFREE)

块使用率 80% ────┼── 移出 Free List(仅允许 UPDATE)

                  │  DELETE(直到 used < PCTUSED)

块使用率 40% ────┘── 重新加入 Free List(接受 INSERT)

4.2 PCTFREE 与 PCTUSED 的协同

关键约束PCTFREE + PCTUSED ≤ 100,推荐 PCTFREE + PCTUSED ≤ 80(留出缓冲)

典型组合

场景PCTFREEPCTUSED效果
静态表585最大化空间利用
平衡型1050默认
频繁 UPDATE3050减少行迁移
高并发插入1030减少 Free List 争用

注意:在**自动段空间管理(ASSM)**表空间中:

  • PCTUSED 不再生效(由 Oracle 自动管理)
  • PCTFREE 仍然有效
  • ASSM 通过位图(Bitmap)管理块空间
-- 查看表空间是否使用 ASSM
SELECT tablespace_name, segment_space_management 
FROM dba_tablespaces;

-- SEGMENT_SPACE_MANAGEMENT = AUTO:ASSM(忽略 PCTUSED)
-- SEGMENT_SPACE_MANAGEMENT = MANUAL:MSSM(PCTUSED 生效)

5. INITRANS 与 MAXTRANS

INITRANS 控制块头 ITL(事务槽)的初始数量,影响并发事务能力[3]:

-- 建表指定
CREATE TABLE high_concurrent_tab (
  id NUMBER
) INITRANS 10 MAXTRANS 255;

-- 索引同样支持
CREATE INDEX idx_emp_dept ON employees(dept_id) 
  INITRANS 20 MAXTRANS 255;
参数默认值作用
INITRANS(表)1块创建时初始 ITL 槽数
INITRANS(索引)2索引块初始 ITL 槽数
MAXTRANS255(19c 固定)最大 ITL 槽数

ITL 槽位与并发

  • 每个并发事务需要 1 个 ITL 槽
  • 如果所有初始 ITL 都被占用,会从空闲空间动态分配(消耗 Free Space)
  • 如果块已满,无法分配新 ITL,事务会等待 → ITL 等待(enq: TX - allocate ITL entry)

何时增大 INITRANS

  • 表并发 UPDATE 频繁
  • 索引并发 INSERT 频繁
  • 检测到 enq: TX - allocate ITL entry 等待事件
-- 查看等待事件
SELECT event, total_waits, time_waited
FROM v$system_event
WHERE event = 'enq: TX - allocate ITL entry';

-- 修复:增大 INITRANS(仅对新建块生效,已有块需重建)
ALTER TABLE high_concurrent_tab INITRANS 20;

-- 对已有数据重建(MOVE)
ALTER TABLE high_concurrent_tab MOVE;
-- 重建索引
ALTER INDEX idx_name REBUILD ONLINE;

6. 行迁移(Row Migration)

6.1 行迁移的成因

当 UPDATE 导致行长度增长,但块中可用空间(PCTFREE 保留空间)不足以容纳新行时,整个行会被迁移到另一个块[4]:

原块(块 A):
+-------------------+
| 行 1              |
| 行 2              |
| 行 3 (已扩展)     | ── 无法容纳扩展后的行
+-------------------+

迁移后:
块 A:
+-------------------+
| 行 1              |
| 行 2              |
| 行 3 (行头+指针)  | ── 指向新位置
+-------------------+

块 B:
+-------------------+
| 行 3 (完整数据)   | ── 实际数据存放位置
+-------------------+

6.2 行迁移的影响

  • 读放大:原本 1 次 I/O 完成的查询,需要 2 次 I/O(先读原块找到指针,再读新块取数据)
  • 性能下降:大量行迁移时,全表扫描性能下降明显
  • 索引性能下降:索引仍指向原块,需额外跳转

6.3 检测行迁移

-- 方法 1:使用 ANALYZE 命令(旧版)
ANALYZE TABLE employees COMPUTE STATISTICS;

SELECT chain_cnt 
FROM user_tables 
WHERE table_name = 'EMPLOYEES';

-- 方法 2:使用 DBMS_STATS(推荐)
EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'EMPLOYEES');

SELECT chain_cnt 
FROM user_tables 
WHERE table_name = 'EMPLOYEES';

-- 方法 3:使用 $UTLCHAIN 视图(精确分析)
ANALYZE TABLE employees LIST CHAINED ROWS;

SELECT * FROM chained_rows 
WHERE table_name = 'EMPLOYEES';

6.4 修复行迁移

-- 方法 1:MOVE 表(重建所有块,需停机)
ALTER TABLE employees MOVE;
-- 重建所有索引
ALTER INDEX idx_emp_name REBUILD;

-- 方法 2:使用 CTAS 重建
CREATE TABLE employees_new AS SELECT * FROM employees;
DROP TABLE employees;
RENAME employees_new TO employees;

-- 方法 3:针对迁移行重建(在线操作)
-- 收集迁移行的 ROWID
CREATE TABLE chained_rows_temp AS
SELECT * FROM chained_rows WHERE table_name = 'EMPLOYEES';

-- 删除并重新插入迁移行
DELETE FROM employees WHERE rowid IN (SELECT head_rowid FROM chained_rows_temp);
INSERT INTO employees SELECT * FROM employees@dblink WHERE rowid IN (SELECT head_rowid FROM chained_rows_temp);

7. 行链接(Row Chaining)

7.1 行链接的成因

当一行数据本身就超过一个块的大小(如包含大 CLOB/BLOB 列),Oracle 会将这一行拆分到多个块中[4]:

块 A:
+-------------------+
| 行 1 (片 1)      | ── 部分数据
| 行 2              |
| 行 3              |
+-------------------+


块 B:
+-------------------+
| 行 1 (片 2)      | ── 继续数据
+-------------------+


块 C:
+-------------------+
| 行 1 (片 3)      | ── 末尾数据
+-------------------+

7.2 行迁移 vs 行链接

特性行迁移(Migration)行链接(Chaining)
触发原因UPDATE 导致行增长行本身大于块大小
数据存放整行复制到新块行被拆分到多块
可避免是(合理 PCTFREE)否(除非增大块大小或拆分表)
解决方法重建表 + 调整 PCTFREE增大块大小 / 拆分 LOB 列

7.3 解决行链接

-- 1. 识别大行表
SELECT table_name, avg_row_len, chain_cnt
FROM user_tables
WHERE chain_cnt > 0
ORDER BY chain_cnt DESC;

-- 2. 调查大列
SELECT column_name, data_type, data_length
FROM user_tab_columns
WHERE table_name = 'LARGE_TABLE'
ORDER BY data_length DESC;

-- 3. 方案 A:将 LOB 列单独存储
ALTER TABLE large_table MOVE 
  LOB (lob_column) STORE AS (TABLESPACE ts_lob);

-- 4. 方案 B:使用更大的块大小表空间
CREATE TABLESPACE ts_16k DATAFILE '/u01/ts16k.dbf' SIZE 1G BLOCKSIZE 16K;
ALTER TABLE large_table MOVE TABLESPACE ts_16k;

8. 高水位线(HWM)

8.1 HWM 的概念

HWM 是段中曾经使用过的最大数据块位置

段内块布局:
+----------+----------+----------+----------+----------+----------+
|  已使用   |  已使用   |  已使用   |  未使用   |  未使用   |  未使用   |
+----------+----------+----------+----------+----------+----------+

                                          HWM(高水位线)
  • HWM 以下的块:曾经被使用,可能为空(DELETE 后)
  • HWM 以上的块:从未使用过
  • 全表扫描会扫描 HWM 以下所有块

8.2 HWM 的影响

  • 全表扫描性能:即使表里只有 1 行,全表扫描也会扫到 HWM
  • DELETE 不降低 HWM:DELETE 后 HWM 保持不变,全表扫描仍慢
  • TRUNCATE 重置 HWM:TRUNCATE 将 HWM 重置为 0

8.3 降低 HWM

-- 方法 1:TRUNCATE(清空表)
TRUNCATE TABLE large_table;

-- 方法 2:MOVE 表(重建段)
ALTER TABLE large_table MOVE;

-- 方法 3:使用 SHRINK SPACE(需启用 ROW MOVEMENT)
ALTER TABLE large_table ENABLE ROW MOVEMENT;
ALTER TABLE large_table SHRINK SPACE;
-- 重建索引
ALTER INDEX idx_large REBUILD;

-- 方法 4:在线重定义(生产推荐)
EXEC DBMS_REDEFINITION.CAN_REDEF_TABLE(...);
EXEC DBMS_REDEFINITION.START_REDEF_TABLE(...);

9. 相关视图与检测命令

9.1 查看表的存储参数

SELECT table_name, pct_free, pct_used, ini_trans, max_trans,
       num_rows, blocks, empty_blocks, avg_space, chain_cnt
FROM user_tables
WHERE table_name = 'EMPLOYEES';
含义
pct_freePCTFREE 值
pct_usedPCTUSED 值
ini_transINITRANS
max_transMAXTRANS
num_rows行数
blocks已使用块数
empty_blocksHWM 下空块数
avg_space块平均空闲空间
chain_cnt链行数

9.2 查看数据块内容

-- 转储数据块内容(用于深度分析)
ALTER SYSTEM DUMP DATAFILE 4 BLOCK 1234;

-- 查看转储文件
-- 路径在 trace 目录下
! ls -lt $ORACLE_BASE/diag/rdbms/orcl/orcl/trace/*.trc | head

9.3 查看段空间使用情况

-- 段空间顾问
SELECT segment_name, segment_type, bytes/1024/1024 AS mb,
       blocks, extents
FROM user_segments
WHERE segment_name = 'EMPLOYEES';

-- 使用 DBMS_SPACE 包查看空间使用
SET SERVEROUTPUT ON
DECLARE
  l_total_blocks NUMBER;
  l_total_bytes  NUMBER;
  l_unused_blocks NUMBER;
  l_unused_bytes  NUMBER;
  l_last_used_extent_file_id NUMBER;
  l_last_used_extent_block_id NUMBER;
  l_last_used_block NUMBER;
BEGIN
  DBMS_SPACE.UNUSED_SPACE(
    segment_owner => USER,
    segment_name => 'EMPLOYEES',
    segment_type => 'TABLE',
    total_blocks => l_total_blocks,
    total_bytes  => l_total_bytes,
    unused_blocks => l_unused_blocks,
    unused_bytes  => l_unused_bytes,
    last_used_extent_file_id => l_last_used_extent_file_id,
    last_used_extent_block_id => l_last_used_extent_block_id,
    last_used_block => l_last_used_block
  );
  DBMS_OUTPUT.PUT_LINE('Total Blocks: ' || l_total_blocks);
  DBMS_OUTPUT.PUT_LINE('Unused Blocks: ' || l_unused_blocks);
END;
/

10. 常见坑与排错

10.1 大量行迁移导致查询变慢

现象:全表扫描时间从 1 秒增加到 10 秒。

排查

SELECT chain_cnt, num_rows, 
       ROUND(chain_cnt/NULLIF(num_rows,0)*100, 2) AS chain_pct
FROM user_tables 
WHERE table_name = 'EMPLOYEES';
-- chain_pct > 5% 需处理

修复

-- 1. 增大 PCTFREE
ALTER TABLE employees PCTFREE 30;

-- 2. MOVE 重建表
ALTER TABLE employees MOVE;
ALTER INDEX idx_emp_id REBUILD;

10.2 enq: TX - allocate ITL entry 等待

现象:高并发场景下出现 ITL 等待。

排查

SELECT event, total_waits, time_waited
FROM v$system_event
WHERE event = 'enq: TX - allocate ITL entry';

修复

-- 增大 INITRANS
ALTER TABLE high_tab INITRANS 20;
-- 重建表使新参数生效
ALTER TABLE high_tab MOVE;

10.3 ORA-01653:表空间扩展失败

现象:INSERT 失败,报表空间不足。

原因:HWM 以下有大量空块(DELETE 后未释放)。

修复

-- 1. SHRINK 表
ALTER TABLE large_tab ENABLE ROW MOVEMENT;
ALTER TABLE large_tab SHRINK SPACE;

-- 2. 或添加数据文件
ALTER TABLESPACE users ADD DATAFILE '/u02/users02.dbf' SIZE 1G;

10.4 PCTFREE 设置过低导致频繁行迁移

现象:表上 UPDATE 操作后,行迁移率持续上升。

修复

-- 1. 评估当前迁移情况
ANALYZE TABLE emp COMPUTE STATISTICS;
SELECT chain_cnt FROM user_tables WHERE table_name='EMP';

-- 2. 增大 PCTFREE 并重建
ALTER TABLE emp PCTFREE 25;
ALTER TABLE emp MOVE;

-- 3. 重建索引
ALTER INDEX idx_emp REBUILD;

10.5 TRUNCATE 后空间未释放到 OS

现象:TRUNCATE 后表空间使用率下降,但 OS 层数据文件大小未变。

原因:TRUNCATE 重置 HWM,但数据文件大小不变(Oracle 保留空间)。

修复

-- 缩小数据文件
ALTER DATABASE DATAFILE '/u01/users01.dbf' RESIZE 500M;

-- 或自动扩展配置
ALTER DATABASE DATAFILE '/u01/users01.dbf' 
  AUTOEXTEND ON NEXT 100M MAXSIZE UNLIMITED;

10.6 LOB 列导致严重行链接

现象:含 LOB 列的表 chain_cnt 极高。

修复

-- 将 LOB 单独存放在 LOB 段
ALTER TABLE docs MOVE 
  LOB (content) STORE AS SECUREFILE lob_seg (
    TABLESPACE ts_lob
    ENABLE STORAGE IN ROW
    CHUNK 8192
    RETENTION
    NOCACHE
  );

11. 最佳实践

11.1 根据业务模式设置 PCTFREE

业务模式PCTFREEPCTUSED
日志表(INSERT only)585
配置表(极少 UPDATE)1050
OLTP 业务表(少量 UPDATE)10-2050
大字段表(频繁扩展)30-4050

11.2 优先使用 ASSM 表空间

-- 创建 ASSM 表空间
CREATE TABLESPACE ts_app 
  DATAFILE '/u01/ts_app.dbf' SIZE 1G
  SEGMENT SPACE MANAGEMENT AUTO;  -- ASSM

-- ASSM 优势:
-- 1. 自动管理 Free List,减少 SEGMENT 争用
-- 2. 无需手动调 PCTUSED
-- 3. 多个进程可同时 INSERT 到不同块

11.3 高并发表增大 INITRANS

-- OLTP 高并发表
CREATE TABLE order_items (
  order_id NUMBER,
  item_id  NUMBER,
  quantity NUMBER
) INITRANS 20 PCTFREE 20;

-- 索引同样配置
CREATE INDEX idx_order ON order_items(order_id) INITRANS 20;

11.4 定期监控行迁移

-- 每周检查行迁移率
SELECT table_name, chain_cnt, num_rows,
       ROUND(chain_cnt/NULLIF(num_rows,0)*100, 2) AS chain_pct
FROM user_tables
WHERE num_rows > 0
  AND chain_cnt > 0
ORDER BY chain_pct DESC;

-- 阈值:chain_pct > 5% 需处理

11.5 大表分区

-- 大表按时间分区,便于管理 HWM
CREATE TABLE sales (
  sale_id NUMBER,
  sale_date DATE,
  amount NUMBER
)
PARTITION BY RANGE (sale_date) (
  PARTITION p_2025_q1 VALUES LESS THAN (TO_DATE('2025-04-01','YYYY-MM-DD')),
  PARTITION p_2025_q2 VALUES LESS THAN (TO_DATE('2025-07-01','YYYY-MM-DD')),
  PARTITION p_2025_q3 VALUES LESS THAN (TO_DATE('2025-10-01','YYYY-MM-DD')),
  PARTITION p_2025_q4 VALUES LESS THAN (TO_DATE('2026-01-01','YYYY-MM-DD'))
);

-- 历史分区可单独 TRUNCATE
ALTER TABLE sales TRUNCATE PARTITION p_2025_q1;

11.6 使用 SEGMENT 顾问

-- 自动段空间顾问
EXEC DBMS_SPACE.AUTO_SPACE_ADVISOR_JOB_PROC;

-- 查看建议
SELECT segment_name, recommendation, 
       space_save_estimate_mb
FROM dba_segment_advisor_recommendations
WHERE segment_owner = USER;

11.7 重建索引避免空间浪费

-- 定期重建碎片化严重的索引
SELECT index_name, height, lf_rows, del_lf_rows,
       ROUND(del_lf_rows/NULLIF(lf_rows,0)*100,2) AS del_pct
FROM index_stats
WHERE del_lf_rows > 0;

-- 高度 > 3 或删除率 > 20% 需重建
ALTER INDEX idx_emp REBUILD ONLINE COMPUTE STATISTICS;

12. 参考资料

[1] Oracle Database Concepts 19c, “Logical Storage Structures” https://docs.oracle.com/en/database/oracle/oracle-database/19/cncpt/logical-storage-structures.html

[2] Oracle Database Administrator’s Guide 19c, “Managing Data Blocks” https://docs.oracle.com/en/database/oracle/oracle-database/19/admin/managing-data-blocks.html

[3] AskTOM, “PCTFREE, PCTUSED and Row Chaining Explained” https://asktom.oracle.com/pls/apex/f?p=100:1:0

[4] 墨天轮,“Oracle 数据块结构与 PCTFREE/PCTUSED 深度解析” https://www.modb.pro/db/1759656813559025664

[5] Oracle Support Note 122020.1, “Detecting and Resolving Row Chaining and Migration” https://support.oracle.com/epmos/faces/DocumentDisplay?id=122020.1

[6] Jonathan Lewis, “Oracle Core: Essential Internals for DBAs and Developers”(Apress, 2011)