【Oracle】ROWID 完全ガイド|構造の読み方・DBMS_ROWID・直接アクセス・重複行削除・バッチ UPDATE・UROWID まで実例で解説

【Oracle】ROWID 完全ガイド|構造の読み方・DBMS_ROWID・直接アクセス・重複行削除・バッチ UPDATE・UROWID まで実例で解説 Oracle

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 の取得
-- テーブルの各行の 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 を分解する

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 を使った直接アクセスパターン
-- ① 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);
インデックス参照も最終的には ROWID アクセス
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 を使った重複削除(どの行を残すか制御できる)
-- 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 範囲に分割して複数バッチに分けて処理すると、トランザクションサイズを制御できます。

ROWID 範囲分割バッチ UPDATE のパターン
-- 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 を格納する列の作成例
-- 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 を保存しておいて後から参照するコードを書いている場合、下記の操作を行った後は保存した 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 の役割についてはインデックス完全ガイドで詳しく解説しています。