Oracle 行迁移(Row Migration)与行链接(Row Chaining)

Oracle 行迁移(Row Migration)与行链接(Row Chaining)

适用版本:Oracle Database 11g / 12c / 19c / 23ai 文档版本:v1.0 / 2026-07


1. 概述

行迁移行链接 是 Oracle 中两种行存储问题[1]:

问题原因影响
行迁移(Migration)UPDATE 后行扩展超过块空间多一次 I/O
行链接(Chaining)行本身超过块大小多次 I/O

2. 行迁移(Row Migration)

2.1 成因

UPDATE 操作导致行长度增长,块中 PCTFREE 保留空间不足:

原块(块 A):
+-------------------------+
| 行 1:sal=5000          | ← 大小 100 字节
| 行 2:sal=4000          |
| 行 3:sal=3000          |
+-------------------------+

UPDATE 行 1,sal=5000,并增加新列:
| 行 1:sal=5000, info=... | ← 大小 500 字节,块空间不足

Oracle 处理:
1. 将整个行 1 迁移到新块(块 B)
2. 块 A 中只保留行 1 的指针
3. 块 A 中行 1 位置标记为 migrated

块 A:
+-------------------------+
| 行 1:[指针]→ 块 B       |
| 行 2                    |
| 行 3                    |
+-------------------------+

块 B:
+-------------------------+
| 行 1:完整数据           |
+-------------------------+

2.2 影响

  • 读放大:查询行 1 需要 2 次 I/O(先读 A 取指针,再读 B 取数据)
  • 全表扫描变慢:扫描 A 仍要跳到 B 取数据
  • 索引性能下降:索引指向 A,需额外跳转

3. 行链接(Row Chaining)

3.1 成因

行本身大小超过数据块大小,必须拆分到多个块:

块大小 8KB,行大小 20KB:

块 A:
+-------------------------+
| 行 1 片段 1(8KB)      | → 指向下一片
+-------------------------+

块 B:
+-------------------------+
| 行 1 片段 2(8KB)      | → 指向下一片
+-------------------------+

块 C:
+-------------------------+
| 行 1 片段 3(4KB)      | → 末尾
+-------------------------+

3.2 常见场景

  • 包含大 LOB 列的行
  • 列数过多的宽表
  • 块大小过小(如 2KB)

4. 行迁移 vs 行链接

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

5. 检测行迁移/行链接

5.1 ANALYZE 命令

-- 收集统计信息
ANALYZE TABLE employees COMPUTE STATISTICS;

-- 查看链行数
SELECT 
  table_name,
  chain_cnt,
  num_rows,
  ROUND(chain_cnt/NULLIF(num_rows,0)*100, 2) AS chain_pct
FROM user_tables
WHERE chain_cnt > 0;
-- chain_pct > 5% 需处理

5.2 LIST CHAINED ROWS

-- 1. 创建 CHAINED_ROWS 表
@$ORACLE_HOME/rdbms/admin/utlchain.sql

-- 2. 分析链行
ANALYZE TABLE employees LIST CHAINED ROWS;

-- 3. 查看链行
SELECT 
  owner_name,
  table_name,
  head_rowid,
  analyze_timestamp
FROM chained_rows
WHERE table_name='EMPLOYEES';

5.3 v$sysstat 统计

SELECT name, value 
FROM v$sysstat 
WHERE name IN (
  'table fetch continued row',  -- 链行读取次数
  'table scan rows gotten'
);

-- table fetch continued row / table scan rows gotten > 1% 需处理

6. 修复行迁移

6.1 调整 PCTFREE

-- 增大 PCTFREE 预留扩展空间
ALTER TABLE employees PCTFREE 30;

-- 重建表使新参数生效
ALTER TABLE employees MOVE;
ALTER INDEX idx_emp_name REBUILD;

6.2 重建表(MOVE)

-- MOVE 重建所有块
ALTER TABLE employees MOVE;

-- 重建索引(MOVE 后索引失效)
SELECT index_name FROM user_indexes WHERE table_name='EMPLOYEES';
ALTER INDEX idx_emp_id REBUILD;
ALTER INDEX idx_emp_name REBUILD;

6.3 CTAS 重建

-- 1. 创建新表
CREATE TABLE employees_new AS SELECT * FROM employees;

-- 2. 删除原表
DROP TABLE employees PURGE;

-- 3. 重命名
RENAME employees_new TO employees;

-- 4. 创建索引和约束

6.4 针对迁移行重建

-- 1. 收集链行 ROWID
ANALYZE TABLE employees LIST CHAINED ROWS;

-- 2. 创建临时表存迁移行
CREATE TABLE chained_emp AS
SELECT * FROM employees 
WHERE rowid IN (SELECT head_rowid FROM chained_rows WHERE table_name='EMPLOYEES');

-- 3. 删除原表中迁移行
DELETE FROM employees 
WHERE rowid IN (SELECT head_rowid FROM chained_rows WHERE table_name='EMPLOYEES');

-- 4. 重新插入
INSERT INTO employees SELECT * FROM chained_emp;

-- 5. 提交
COMMIT;

7. 修复行链接

7.1 增大块大小

-- 1. 创建大块表空间
ALTER SYSTEM SET db_16k_cache_size=512M SCOPE=BOTH;

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

-- 2. 移动表
ALTER TABLE large_table MOVE TABLESPACE ts_16k;

7.2 LOB 单独存储

-- LOB 列单独存放,减少主表行大小
ALTER TABLE docs MOVE 
  LOB (content) STORE AS SECUREFILE (
    TABLESPACE ts_lob
    ENABLE STORAGE IN ROW  -- 小 LOB 内联,大 LOB 外存
    CHUNK 8192
    RETENTION
    NOCACHE
  );

7.3 拆分宽表

-- 原宽表
-- CREATE TABLE wide_tab (id, col1, col2, ..., col50);

-- 拆分为多个表
-- CREATE TABLE main_tab (id, col1, col2, ...);
-- CREATE TABLE ext_tab (id, col30, col31, ...);

8. 预防措施

8.1 合理设置 PCTFREE

场景PCTFREE
只读表0-5
平衡型10(默认)
频繁 UPDATE20-30
大字段扩展30-40

8.2 选择合适的块大小

场景推荐块大小
OLTP8KB(默认)
数据仓库16KB / 32KB
含大行表16KB / 32KB
索引8KB

8.3 LOB 单独存储

-- LOB 列单独表空间
CREATE TABLE docs (
  id NUMBER,
  content CLOB
) LOB (content) STORE AS SECUREFILE (
  TABLESPACE ts_lob
  ENABLE STORAGE IN ROW
);

9. 监控脚本

-- 定期检查行迁移率
SELECT 
  table_name,
  num_rows,
  chain_cnt,
  ROUND(chain_cnt/NULLIF(num_rows,0)*100, 2) AS chain_pct,
  CASE 
    WHEN chain_cnt/NULLIF(num_rows,0) > 0.05 THEN 'NEED FIX'
    ELSE 'OK'
  END AS status
FROM user_tables
WHERE num_rows > 0
ORDER BY chain_pct DESC;

10. 常见坑与排错

10.1 全表扫描慢

现象:DELETE/UPDATE 后表变小但查询变慢。

原因:行迁移导致读放大。

修复

-- 1. 检查行迁移率
ANALYZE TABLE large_tab COMPUTE STATISTICS;
SELECT table_name, chain_cnt, num_rows 
FROM user_tables WHERE table_name='LARGE_TAB';

-- 2. 重建
ALTER TABLE large_tab MOVE;
ALTER INDEX idx REBUILD;

10.2 LOB 表 chain_cnt 高

修复

-- 将 LOB 单独存储
ALTER TABLE docs MOVE 
  LOB (content) STORE AS SECUREFILE (TABLESPACE ts_lob);

10.3 MOVE 后索引失效

现象:MOVE 后查询变慢。

原因:MOVE 会使索引失效。

修复

-- 重建所有索引
SELECT 'ALTER INDEX ' || index_name || ' REBUILD;' 
FROM user_indexes 
WHERE table_name='EMPLOYEES';

10.4 SHRINK SPACE 失败

修复

-- 启用行移动
ALTER TABLE large_tab ENABLE ROW MOVEMENT;

-- SHRINK
ALTER TABLE large_tab SHRINK SPACE CASCADE;

11. 最佳实践

  1. 根据业务设置 PCTFREE:UPDATE 频繁的表 PCTFREE=20-30
  2. LOB 单独存储:避免主表行过大
  3. 定期监控 chain_cnt:> 5% 需处理
  4. 使用 MOVE 重建:定期维护
  5. TRUNCATE 优于 DELETE:清空表用 TRUNCATE
  6. 合理块大小:大行表用 16K/32K
  7. 避免过宽表:列数过多考虑拆分
  8. ASSM 表空间:减少段空间争用

12. 参考资料

[1] Oracle Database Administrator’s Guide 19c, “Row Chaining and Migrating” https://docs.oracle.com/en/database/oracle/oracle-database/19/admin/managing-tables.html

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