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(默认) |
| 频繁 UPDATE | 20-30 |
| 大字段扩展 | 30-40 |
8.2 选择合适的块大小
| 场景 | 推荐块大小 |
|---|---|
| OLTP | 8KB(默认) |
| 数据仓库 | 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. 最佳实践
- 根据业务设置 PCTFREE:UPDATE 频繁的表 PCTFREE=20-30
- LOB 单独存储:避免主表行过大
- 定期监控 chain_cnt:> 5% 需处理
- 使用 MOVE 重建:定期维护
- TRUNCATE 优于 DELETE:清空表用 TRUNCATE
- 合理块大小:大行表用 16K/32K
- 避免过宽表:列数过多考虑拆分
- 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