導入(要約)
oracle のデータブロックの行データ格納は「ブロック・ヘッダー/ディレクトリ/データ/空き領域」で構成され、更新や行サイズ拡大で行の連鎖・移動が起き得ます。本稿は仕組み→最短確認手順→対処の順で、初心者でも迷わない実機検証付きで解説します。
💰 【PR】Oracleエンジニアの市場価値、調べてみませんか?
Oracleのスキルは需要が高く、特定の資格や経験を持っていると年収が大幅にアップするケースがあります。まずはIT専門のエージェントで非公開求人をチェックしてみませんか?
結論・最短手順(スニペット用)
- ブロック構造を理解:ヘッダー(ITL 等)/テーブル・行ディレクトリ/データ/空き領域。
- 現状を把握:
v$parameterでdb_block_size、ANALYZE ... LIST CHAINED ROWSで連鎖/移動の有無を調査。 - 原因を切り分け:
- 行そのものが1ブロックに収まらない→行の連鎖。
- 更新で元ブロックに入らなくなった→行の移動(元位置に転送ポインタ)。
- 対処の基本:
PCTFREEの見直し/行設計の見直し(大カラムの分割・LOB適正化)/必要なら表の再編成(ALTER TABLE ... MOVE等※ROWID変化に注意)。 - 継続監視:必要時に
ANALYZE ... LIST CHAINED ROWS(UTLCHAIN.SQL)を使い、発生傾向を把握。DBMS_STATSはCHAIN_CNTを埋めない点に留意。
背景と基礎
データブロックとは?
Oracle の論理ストレージ最小単位。1つのブロック内に複数行の行ピースが格納され、行位置は行ディレクトリから参照されます。ROWID の内部構成(ファイル番号/ブロック番号/行番号)がこれに対応します。
ブロックの内部構造(テキスト図)
+-------------------------+
|ブロック・ヘッダー(共通/可変ヘッダー, ITL 等の管理情報)|
+-------------------------+
|テーブル・ディレクトリ|行ディレクトリ(行ポインタ配列)|
+-------------------------+
| デ ー タ 領 域 |
| [行ピース/行ヘッダー][列データ] ... |
+-------------------------+
| 空 き 領 域(PCTFREE) |
+-------------------------+
- ITL(Interested Transaction List):同一ブロックでのトランザクション競合を捌くスロット。
- 行ヘッダーと NULL:行中の末尾の連続する NULL は保存されません。途中の NULL は長さ 0 を表す1バイトを要します(可変長列の場合)。
「行の連鎖」と「行の移動」とは?
- 行の連鎖(Row Chaining):1行が1ブロックに収まらないため、行ピースが複数ブロックに分割される。典型例は多くの大きな可変長列(例:
VARCHAR2(4000)を複数)や拡張文字列。 - 行の移動(Row Migration):更新で行サイズが増え、同じブロックに入らなくなったときに行全体が別ブロックへ移動。元位置には転送アドレス(フォワーディング・ポインタ)が残り、ROWID は原則不変(ただし表 MOVE 等では変化)。
一口メモ:LOB列は通常「ロケータ」を行内に持ち本体は別セグメント(SecureFiles 既定)。「LOBが大きい=行の連鎖」とは限りません。
手順・実装(実機で通る最短コース・確認まで詳説)
前提(19c 非CDB / Oracle Linux / 既定ブロックサイズ想定):
SYSで SQL*Plus もしくは SQLcl が使えること- 例では汎用表領域
USERSを使用(必要に応じて読み替え)
0) 事前確認:ブロックサイズの把握(参照のみ)
目的:自環境の
db_block_sizeを確認し、後続の再現の妥当性を判断する。
-- ブロックサイズ(バイト)
SELECT value AS db_block_size_bytes
FROM v$parameter
WHERE name = 'db_block_size';
解説:データベース作成時に決まる既定ブロックサイズ(例:8192=8KB)。行が1ブロックに収まるかの目安になります。ここは参照のみで安全です。
1) 検証用ユーザーの作成(権限とクォータを付与)
目的:作業を分離し、権限不足やクォータ不足の切り分けを容易にする。変更系(ユーザー作成)なので、不要になったら削除可能。
-- SYS で実行(※パスワードは環境方針に合わせて変更)
CREATE USER blkdemo IDENTIFIED BY Demo#1234
DEFAULT TABLESPACE users
TEMPORARY TABLESPACE temp;
GRANT CREATE SESSION, CREATE TABLE TO blkdemo;
ALTER USER blkdemo QUOTA UNLIMITED ON users;
解説:CREATE USER で作業用スキーマを用意。CREATE TABLE 権限と USERS 表領域のクォータを付与します。ここまでは他オブジェクトへの影響なし。
2) 行の連鎖を再現する表の作成とデータ投入
目的:1行がブロックに収まらない状況を意図的に作る(行の連鎖を誘発)。
-- BLKDEMO で実行(以降はこのスキーマ)
CREATE TABLE t_chain_demo (
id NUMBER PRIMARY KEY,
c1 VARCHAR2(4000),
c2 VARCHAR2(4000)
) PCTFREE 10; -- 既定のまま
INSERT INTO t_chain_demo
SELECT LEVEL,
RPAD('X',4000,'X'),
RPAD('Y',4000,'Y')
FROM dual CONNECT BY LEVEL <= 10;
COMMIT;
解説:VARCHAR2(4000) を2列に満杯で格納し、8KB ブロックでは1行が収まらない状況を作ります。RPAD で固定長の文字列を生成。PCTFREE 10 は挿入時に 10% を将来更新用に残す指定で、ここでは既定値。
3) 行の移動を再現する表の作成と更新
目的:更新で行サイズを増やし、元ブロックに収まらない=行の移動を観察。
CREATE TABLE t_mig_demo (
id NUMBER PRIMARY KEY,
pad VARCHAR2(1000)
) PCTFREE 0; -- 更新余地を極小化して移動を誘発
INSERT INTO t_mig_demo
SELECT LEVEL, RPAD('A',10,'A')
FROM dual CONNECT BY LEVEL <= 1000;
COMMIT;
-- 1行だけ極端に膨らませる(移動を起こしやすい)
UPDATE t_mig_demo
SET pad = RPAD('B',1000,'B')
WHERE id = 1;
COMMIT;
解説:PCTFREE 0 によりブロック内の空きを残さず挿入→後から pad を 10→1000 へ拡大し、元ブロックに空きがなく移動が起こりやすくなります。
4) 検出テーブル(CHAINED_ROWS)の作成
目的:
ANALYZE ... LIST CHAINED ROWSの出力先であるCHAINED_ROWS表を一度だけ作成。参照系作業(分析)自体は安全。
-- SYS で一度だけ実行:UTLCHAIN.SQL により CHAINED_ROWS を作成
-- パスは ORACLE_HOME に依存
@?/rdbms/admin/utlchain.sql
解説:スクリプトは CHAINED_ROWS 表(所有者は実行ユーザー)と同義語を作ります。既に存在する場合はスキップされます。
💰 【PR】Oracleエンジニアの市場価値、調べてみませんか?
Oracleのスキルは需要が高く、特定の資格や経験を持っていると年収が大幅にアップするケースがあります。まずはIT専門のエージェントで非公開求人をチェックしてみませんか?
5) 連鎖/移動の検出(ANALYZE)と結果の確認
目的:対象表で連鎖・移動がある行を
CHAINED_ROWSに抽出し、件数と実行例(ROWID)を確認する。
-- BLKDEMO で実行:検出結果を CHAINED_ROWS に出力(表の内容は変更しない)
ANALYZE TABLE blkdemo.t_chain_demo LIST CHAINED ROWS;
ANALYZE TABLE blkdemo.t_mig_demo LIST CHAINED ROWS;
解説:LIST CHAINED ROWS は行の連鎖と移動の両方を検出します。対象表のデータ自体は変更しません(安全な参照系)。
-- 件数の把握
SELECT owner_name,
table_name,
COUNT(*) AS rows_flagged
FROM chained_rows
WHERE owner_name = 'BLKDEMO'
GROUP BY owner_name, table_name
ORDER BY table_name;
解説:表ごとの連鎖/移動フラグ件数を確認します。T_CHAIN_DEMO で複数、T_MIG_DEMO で 1 件程度のヒットが見込まれます。
コマンド実行結果
SQL> SELECT owner_name,
2 table_name,
3 COUNT(*) AS rows_flagged
4 FROM chained_rows
5 WHERE owner_name = 'BLKDEMO'
6 GROUP BY owner_name, table_name
7 ORDER BY table_name;
OWNER_N TABLE_NAME ROWS_FLAGGED
------- -------------------- ------------
BLKDEMO T_CHAIN_DEMO 10
BLKDEMO T_MIG_DEMO 1
-- サンプルの HEAD_ROWID を確認(先頭 5 行)
SELECT table_name, head_rowid
FROM chained_rows
WHERE owner_name = 'BLKDEMO'
FETCH FIRST 5 ROWS ONLY;
解説:HEAD_ROWID は行ピースの先頭が格納されている場所(元ブロック)の ROWID を示します。移動行では元位置に転送ポインタが残っているため、ここに記録されます。
コマンド実行結果
SQL> SELECT table_name, head_rowid
2 FROM chained_rows
3 WHERE owner_name = 'BLKDEMO'
4 FETCH FIRST 5 ROWS ONLY;
TABLE_NAME HEAD_ROWID
-------------------- ------------------
T_CHAIN_DEMO AAAR4BAAHAAAAFbAAA
T_CHAIN_DEMO AAAR4BAAHAAAAFdAAA
T_CHAIN_DEMO AAAR4BAAHAAAAFoAAA
T_CHAIN_DEMO AAAR4BAAHAAAAFpAAA
T_CHAIN_DEMO AAAR4BAAHAAAAFsAAA
-- ROWID の内訳(ファイル番号/ブロック番号/行番号)を分解して見る
SELECT table_name,
head_rowid,
DBMS_ROWID.ROWID_RELATIVE_FNO(head_rowid) AS file_no,
DBMS_ROWID.ROWID_BLOCK_NUMBER(head_rowid) AS block_no,
DBMS_ROWID.ROWID_ROW_NUMBER(head_rowid) AS row_no
FROM chained_rows
WHERE owner_name = 'BLKDEMO'
AND ROWNUM <= 5;
解説:DBMS_ROWID を使って、ROWID 内部のファイル/ブロック/行番号を可視化します。I/O 観点の理解に役立ちます。
コマンド実行結果
SQL> SELECT table_name,
2 head_rowid,
3 DBMS_ROWID.ROWID_RELATIVE_FNO(head_rowid) AS file_no,
4 DBMS_ROWID.ROWID_BLOCK_NUMBER(head_rowid) AS block_no,
5 DBMS_ROWID.ROWID_ROW_NUMBER(head_rowid) AS row_no
6 FROM chained_rows
7 WHERE owner_name = 'BLKDEMO'
8 AND ROWNUM <= 5;
TABLE_NAME HEAD_ROWID FILE_NO BLOCK_NO ROW_NO
-------------------- ------------------ ---------- ---------- ----------
T_CHAIN_DEMO AAAR4BAAHAAAAFbAAA 7 347 0
T_CHAIN_DEMO AAAR4BAAHAAAAFdAAA 7 349 0
T_CHAIN_DEMO AAAR4BAAHAAAAFoAAA 7 360 0
T_CHAIN_DEMO AAAR4BAAHAAAAFpAAA 7 361 0
T_CHAIN_DEMO AAAR4BAAHAAAAFsAAA 7 364 0
-- (参考)連鎖/移動の行を実データと突き合わせる
SELECT 'CHAIN' AS cause, t.id, t.c1, t.c2
FROM blkdemo.t_chain_demo t
WHERE t.ROWID IN (
SELECT head_rowid
FROM chained_rows
WHERE owner_name = 'BLKDEMO'
AND table_name = 'T_CHAIN_DEMO'
)
UNION ALL
SELECT 'MIG', m.id, m.pad, CAST(NULL AS VARCHAR2(1))
FROM blkdemo.t_mig_demo m
WHERE m.ROWID IN (
SELECT head_rowid
FROM chained_rows
WHERE owner_name = 'BLKDEMO'
AND table_name = 'T_MIG_DEMO'
);
解説:CHAINED_ROWS.HEAD_ROWID と表の ROWID を突合し、どの行が該当するかを特定します。移動行は元位置の ROWID が返る点がポイントです。
補足:連鎖か移動かの厳密判別はブロックダンプ等の深掘りで可能ですが、運用上は「1行が大きすぎる設計(連鎖)」か「更新で膨らんだ(移動)」かを表定義・更新パターンから判断し、対策(
PCTFREE/列設計/再編成)に落とし込むのが実務的です。
実行例(読み取り方のポイント)
v$parameter:db_block_size_bytes = 8192(例)CHAINED_ROWS:T_CHAIN_DEMOに複数行、T_MIG_DEMOに 1 行程度の記録が出力されやすい。
実務の読み取り方:
- 行の連鎖は必ず複数ブロックアクセスになるため、全表走査やランダムアクセス時の I/O 増に直結。
- 行の移動は元ブロック→転送先の2回アクセスになり、索引アクセスでも追いかけが発生。CBO コスト推定が拾いにくい場合があります。
パフォーマンス最適化のコツ(要点)
PCTFREEを見直す:頻繁に列が伸びる表はPCTFREEを高めに確保して移動を抑制。- 行設計を見直す:巨大な可変長列が多い行は列分割や正規化を検討。
- LOB の適正化:LOB は SecureFiles(既定)を用い、行内/行外の格納方針を理解。
- 再編成は計画的に:
ALTER TABLE ... MOVE [UPDATE INDEXES]等は ROWID が変わる(IOT など例外あり)。アプリが ROWID 依存なら要注意。
トラブルシューティング(代表的な事象)
| 症状/エラー | 可能性 | 確認/対処の順 |
|---|---|---|
CHAINED_ROWS 参照時に ORA-01495 | CHAINED_ROWS 表が未作成/権限不足 | @?/rdbms/admin/utlchain.sql を SYS で実行→表と同義語を作成。 |
ORA-01496 | CHAINED_ROWS 表の定義不一致 | バージョンに合う UTLCHAIN.SQL を再作成。 |
| 連鎖/移動が多い | 行設計/更新パターン/PCTFREE 未最適 | ANALYZE ... LIST CHAINED ROWS で具体行を確認→行設計・PCTFREE・LOB 方針を見直し。 |
| 再編成後に性能悪化 | 索引と統計の不整合 | ALTER TABLE ... MOVE UPDATE INDEXES を活用/統計再収集。 |
運用・監視・セキュリティ上の注意
- ROWIDを長期キーにしない:表の MOVE / SHRINK / 物理再配置でROWID は変化し得ます。業務キーは論理主キーで。
- 分割統治:発生率が高い表は DDL ログ/変更履歴と併せて原因(データモデリング/アプリ更新)を特定。
- 参照系での確認優先:
ANALYZE ... LIST CHAINED ROWSは本体を書き換えず安全に傾向把握が可能。
FAQ
Q1. ブロックサイズを後から 8KB→16KB に変えれば連鎖は減りますか?
A. 既存データベースの既定ブロックサイズは後から変更できません。新 DB 設計や非既定サイズの表領域+専用バッファなど高度な構成が必要です。まずは行設計と PCTFREE の見直しが基本です。
Q2. LOB は連鎖の主因ですか?
A. 多くの場合、LOB 本体は行外(別セグメント)で、行内にはロケータのみです。連鎖は巨大な可変長列の組合せで起きやすいです。
Q3. DBA_TABLES.CHAIN_CNT は DBMS_STATS で更新されますか?
A. 原則更新されません。把握には ANALYZE ... LIST CHAINED ROWS や ANALYZE ... COMPUTE STATISTICS を利用します。
Q4. 行の移動で ROWID は常に不変?
A. 原則不変ですが、表 MOVE/SHRINK、パーティション移動(ROW MOVEMENT 有効時)などで変化するケースがあります。
まとめ(要点)
- データブロックはヘッダー/ディレクトリ/データ/空き領域の4層。ITL と行ディレクトリが鍵。
- 連鎖=1行が1ブロック不可能/移動=更新で元ブロックに入らない。
- 検出は
ANALYZE ... LIST CHAINED ROWSが確実。DBMS_STATSはCHAIN_CNTを埋めない。 - 対策は
PCTFREEと行設計の見直しが第一。LOB はロケータ/行外格納を理解。 - 再編成はROWID変化に注意し、統計/索引と併せて計画的に。
本記事は Oracle Database 19c を対象に解説します(他バージョンは画面や既定値が異なる場合があります)。
[参考]
Oracle Database データベース概要 19c

💰 【PR】Oracleエンジニアの市場価値、調べてみませんか?
Oracleのスキルは需要が高く、特定の資格や経験を持っていると年収が大幅にアップするケースがあります。まずはIT専門のエージェントで非公開求人をチェックしてみませんか?



コメント