ROWID は Oracle が各行に付ける物理アドレスです。テーブルをどんな方法で検索するときでも、最終的に Oracle はその行の ROWID を特定してデータブロックに直接アクセスします。ROWID を理解することで、インデックスの仕組み・重複行の削除・大量データの並列更新など、実務でよく使うテクニックの背景が見えてきます。
- ROWID の構造:18文字の拡張 ROWID の4フィールドの読み方
- DBMS_ROWID パッケージで ROWID を分解・生成する方法
- WHERE ROWID = … による最速の直接アクセス
- 相関サブクエリ・ROW_NUMBER を使った重複行の削除パターン
- ROWID 範囲でテーブルを分割する並列バッチ UPDATE
- UROWID(索引構成表・クラスタ表)と通常 ROWID の違い
- ROWID が変わる操作(MOVE・SHRINK・import)の注意点
ROWID とは
ROWID はヒープ編成表(通常のテーブル)の各行の物理的な格納場所を示します。SELECT ROWID, ... で取得でき、ROWID を直接 WHERE 句に指定するとインデックスや全表スキャンを経由せず、そのブロックへ直接アクセスします。
-- テーブルの各行の ROWID を取得する SELECT ROWID, employee_id, last_name FROM employees FETCH FIRST 5 ROWS ONLY; -- 結果例(拡張ROWID: 18文字の Base64 文字列) -- ROWID EMPLOYEE_ID LAST_NAME -- AAAZAAAAEAAAABxAAA 100 King -- AAAZAAAAEAAAABxAAB 101 Kochhar -- AAAZAAAAEAAAABxAAC 102 De Haan -- AAAZAAAAEAAAABxAAD 103 Hunold -- AAAZAAAAEAAAABxAAE 104 Ernst
拡張 ROWID の構造(18文字の読み方)
Oracle 8 以降の拡張 ROWID は Base64 でエンコードされた 18 文字で、4 つのフィールドに分かれています。
| フィールド | 文字数 | 内容 | 例(AAAZAAAAEAAAABXAAA) |
|---|---|---|---|
| データオブジェクト番号 | 6 | テーブル・パーティションの論理的な識別番号 | AAAZAA |
| 相対ファイル番号 | 3 | 表領域内のデータファイル番号(1〜1023) | AAE |
| ブロック番号 | 6 | ファイル内のデータブロック番号 | AAAABX |
| 行番号 | 3 | ブロック内の行スロット番号(0 始まり) | AAA |
ROWID が分かれば「どの表領域・どのファイル・何ブロック目・何行目」が特定でき、Oracle はバッファキャッシュを経由してそのブロックを直接読み込みます。
DBMS_ROWID パッケージで ROWID を分解する
-- ROWID を各フィールドに分解する
SELECT
ROWID,
DBMS_ROWID.ROWID_OBJECT(ROWID) AS object_no, -- データオブジェクト番号
DBMS_ROWID.ROWID_RELATIVE_FNO(ROWID) AS file_no, -- 相対ファイル番号
DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID) AS block_no, -- ブロック番号
DBMS_ROWID.ROWID_ROW_NUMBER(ROWID) AS row_no -- ブロック内の行番号(0始まり)
FROM employees
WHERE employee_id = 100;
-- オブジェクト番号から表名を確認
SELECT object_name, object_type
FROM dba_objects
WHERE data_object_id = 73482; -- ↑ で得たオブジェクト番号
-- ROWID からブロック番号の範囲を調べる(パーティションやセグメント確認に使う)
SELECT
DBMS_ROWID.ROWID_BLOCK_NUMBER(MIN(ROWID)) AS min_block,
DBMS_ROWID.ROWID_BLOCK_NUMBER(MAX(ROWID)) AS max_block,
COUNT(*) AS total_rows
FROM employees;
ROWID による直接アクセス(最速の行取得)
ROWID を WHERE 句に指定すると、Oracle は EXPLAIN PLAN 上で TABLE ACCESS BY USER ROWID と表示され、インデックスも全表スキャンも使わずにそのブロックへ直接アクセスします。これは Oracle の行アクセス方法の中で最速です。
-- ① ROWID を取得して変数に保存し、後で直接アクセスする
DECLARE
v_rowid ROWID;
v_name employees.last_name%TYPE;
BEGIN
-- ROWID を取得(FOR UPDATE で行をロックしながら ROWID も保存)
SELECT ROWID, last_name INTO v_rowid, v_name
FROM employees
WHERE employee_id = 100
FOR UPDATE;
-- ROWID で直接 UPDATE(インデックスを使わず最速)
UPDATE employees
SET salary = salary * 1.1
WHERE ROWID = v_rowid;
COMMIT;
END;
/
-- ② ROWID を文字列として保存・後から取得する(CHAR(18) で格納)
-- 用途: 処理対象行の一覧をテーブルに保存して後でバッチ処理する
CREATE TABLE work_rowids (
rid UROWID -- ROWID を格納する専用型
);
INSERT INTO work_rowids
SELECT ROWID FROM orders WHERE order_date < SYSDATE - 365;
-- 保存した ROWID で直接 UPDATE
UPDATE orders o
SET o.status = 'ARCHIVED'
WHERE ROWID IN (SELECT rid FROM work_rowids);
Oracle のインデックス(B-Tree)の葉ブロックにはキー値 + ROWID が格納されています。インデックスを使ったアクセスは、まずインデックスでキーを探して ROWID を取得し、次に ROWID でテーブルブロックへアクセスする2段階の処理です。ROWID を直接指定すると、インデックス参照の第1段階を省略できます。
ROWID を使った重複行の削除
同一データが複数行あるとき、各グループの最小 ROWID を持つ1行を残して他を削除する手法が定番です。
-- 重複確認: email が重複している行を探す
SELECT email, COUNT(*) AS cnt
FROM employees
GROUP BY email
HAVING COUNT(*) > 1;
-- ROWID を使った重複削除: 同じ email のうち最小 ROWID を持つ行だけ残す
DELETE FROM employees e1
WHERE ROWID > (
SELECT MIN(ROWID)
FROM employees e2
WHERE e1.email = e2.email
);
-- 削除前に対象件数を確認する(DELETE 前に SELECT で件数チェック)
SELECT COUNT(*) AS delete_count
FROM employees e1
WHERE ROWID > (
SELECT MIN(ROWID)
FROM employees e2
WHERE e1.email = e2.email
);
COMMIT;
-- ROW_NUMBER を使うと「最新の更新日を持つ行を残す」など細かく制御できる
DELETE FROM employees
WHERE ROWID IN (
SELECT ROWID FROM (
SELECT
ROWID,
ROW_NUMBER() OVER (
PARTITION BY email
ORDER BY hire_date DESC -- 入社日が新しい行を残す(行1以外を削除)
) AS rn
FROM employees
)
WHERE rn > 1 -- 2行目以降が重複行
);
COMMIT;
-- 削除後の確認(重複がなくなったことを確認)
SELECT email, COUNT(*) FROM employees
GROUP BY email HAVING COUNT(*) > 1;
-- → 行が返らなければ重複なし
ROWID 範囲分割による並列バッチ UPDATE
数千万行のテーブルを一括 UPDATE すると UNDO の肥大化・ロールバックリスクがあります。DBMS_ROWID でテーブルを均等な ROWID 範囲に分割して複数バッチに分けて処理すると、トランザクションサイズを制御できます。
-- DBMS_PARALLEL_EXECUTE を使った ROWID 分割バッチ処理
-- (Oracle 11g R2 以降)
DECLARE
v_task VARCHAR2(50) := 'UPDATE_SALARY_BATCH';
v_sql VARCHAR2(500);
v_status NUMBER;
BEGIN
-- タスクを作成
DBMS_PARALLEL_EXECUTE.CREATE_TASK(v_task);
-- テーブルを ROWID 範囲で分割(1チャンクあたり約 10000 行)
DBMS_PARALLEL_EXECUTE.CREATE_CHUNKS_BY_ROWID(
task_name => v_task,
table_owner => USER,
table_name => 'EMPLOYEES',
by_row => TRUE,
chunk_size => 10000 -- 1バッチの行数
);
-- 各チャンク(ROWID 範囲)で UPDATE を実行
v_sql := 'UPDATE employees
SET salary = salary * 1.05
WHERE ROWID BETWEEN :start_id AND :end_id';
DBMS_PARALLEL_EXECUTE.RUN_TASK(
task_name => v_task,
sql_stmt => v_sql,
language_flag => DBMS_SQL.NATIVE,
parallel_level => 4 -- 4並列で実行
);
-- 完了状態を確認
SELECT status INTO v_status
FROM user_parallel_execute_tasks
WHERE task_name = v_task;
IF v_status = DBMS_PARALLEL_EXECUTE.FINISHED THEN
DBMS_OUTPUT.PUT_LINE('完了');
COMMIT;
ELSE
DBMS_OUTPUT.PUT_LINE('エラーあり: ステータス=' || v_status);
ROLLBACK;
END IF;
DBMS_PARALLEL_EXECUTE.DROP_TASK(v_task);
END;
/
UROWID:索引構成表・クラスタ表での ROWID
通常のヒープ表(HEAP TABLE)では ROWID が物理アドレスを表しますが、特殊なテーブルでは物理アドレスが存在しないため、UROWID(Universal ROWID)が使われます。
| テーブル種別 | ROWID の型 | ROWID の内容 |
|---|---|---|
| ヒープ表(通常テーブル) | ROWID | 物理的なブロックアドレス(18文字) |
| 索引構成表(IOT) | UROWID | 主キー値に基づく論理アドレス(可変長) |
| クラスタ表 | ROWID | クラスタキー+ブロックアドレスの形式 |
| 外部表 | UROWID | 行番号ベースの論理アドレス |
-- UROWID 型の列は通常テーブルにも格納できる
-- 用途: 処理済み行の ROWID を一時保存するワーク表
CREATE TABLE batch_processed_rows (
batch_id NUMBER,
target_rowid UROWID, -- ヒープ表の ROWID も IOT の UROWID も格納できる
processed_at DATE DEFAULT SYSDATE
);
-- IOT の ROWID を確認すると論理アドレスになっていることが分かる
CREATE TABLE iot_sample (
id NUMBER PRIMARY KEY,
name VARCHAR2(50)
) ORGANIZATION INDEX; -- IOT として作成
INSERT INTO iot_sample VALUES (1, 'AAA');
INSERT INTO iot_sample VALUES (2, 'BBB');
COMMIT;
SELECT ROWID, id, name FROM iot_sample;
-- IOT の ROWID は可変長の文字列(主キー値が含まれる論理アドレス)
ROWID が変わる操作の注意点
ROWID を保存しておいて後から参照するコードを書いている場合、下記の操作を行った後は保存した ROWID が無効になります。
- ALTER TABLE … MOVE:テーブルを別のブロックに移動するため全行の ROWID が変わる。実行後はインデックスが UNUSABLE になる(
ALTER INDEX ... REBUILDが必要) - ALTER TABLE … SHRINK SPACE:HWM を縮小する際に行の移動が発生し ROWID が変わる場合がある
- export → import(Data Pump):インポート先の物理配置が変わるため ROWID が変わる
- パーティション操作(SPLIT・MOVE PARTITION):パーティション内の行が移動して ROWID が変わる
- 同一トランザクション内:MOVE や SHRINK を行わない限り、トランザクション内では ROWID は変わらない
まとめ
- ROWID の構造:18文字の Base64 文字列。オブジェクト番号(6)+ ファイル番号(3)+ ブロック番号(6)+ 行番号(3)
- DBMS_ROWID:ROWID_OBJECT・ROWID_BLOCK_NUMBER・ROWID_ROW_NUMBER などで ROWID を数値に変換できる
- 直接アクセス:
WHERE ROWID = ...は最速のアクセス方法。インデックスも全表スキャンも不要 - 重複削除:
DELETE WHERE ROWID > (SELECT MIN(ROWID) ...)が標準パターン。残す行の制御には ROW_NUMBER を使う - バッチ更新:DBMS_PARALLEL_EXECUTE で ROWID 範囲に分割して並列処理できる
- MOVE 後は ROWID が無効:保存した ROWID を後から使うコードでは、MOVE・SHRINK・import 後に再取得が必要
ROWID を活用した重複削除の基本パターンは重複データを削除する方法完全ガイドも参照してください。インデックスの葉ブロックに格納される ROWID の役割についてはインデックス完全ガイドで詳しく解説しています。

