【PL/SQL】本番データ補正スクリプトの作り方|UPDATE前確認・バックアップ・ロールバック手順

【PL/SQL】本番データ補正スクリプトの作り方|UPDATE前確認・バックアップ・ロールバック手順 PL/SQL

本番データを補正する作業は、SQLそのものよりも作業設計が重要です。条件を少し間違えただけで想定外の行を更新したり、更新後に元の値が分からなくなったり、作業証跡が残らなかったりします。特にOracle環境では、DDLが暗黙コミットになる点や、実行中のトランザクション管理にも注意が必要です。

この記事では、PL/SQLとSQLを使って本番データ補正スクリプトを安全に作るための考え方を整理します。単発のUPDATE文ではなく、事前確認、対象行の固定、バックアップ、差分確認、更新件数チェック、ロールバック、作業ログまでを1つの流れとして扱います。トランザクション全体の考え方は PL/SQLのトランザクション設計、部分的な取り消しは SAVEPOINTで部分ロールバックを実装する方法 もあわせて参考になります。

この記事で扱うこと

  • データ補正で事故が起きる典型パターン
  • UPDATE前の対象件数確認
  • 対象行を作業テーブルに固定する方法
  • 補正前データのバックアップ
  • 差分確認と更新件数チェック
  • ロールバックSQLの作り方
  • 作業証跡と監査ログの残し方
スポンサーリンク

データ補正で事故が起きる原因

本番データ補正の失敗は、複雑なSQLよりも基本的な確認不足から起きます。たとえば、対象条件に日付やステータスを入れ忘れる、同じ顧客に複数明細があることを考慮しない、実行直前に対象データが増えている、更新件数を確認せずにコミットする、といったものです。

対象行が固定されていない確認時点と実行時点で対象が変わり、想定外の行まで更新されます。
元データを残していない更新後に問い合わせが来ても、補正前の値を確認できません。
更新件数を検証していない0件更新や想定以上の更新に気づかないままコミットしてしまいます。
ロールバック手順がない失敗に気づいても、どの値へ戻せばよいか分からなくなります。
証跡が弱い誰が、いつ、なぜ、何件直したかを後から説明できません。

つまり、補正スクリプトではUPDATE文を書く前に、対象を固定し、戻せる状態を作ることが先です。この順番を守るだけで、作業リスクはかなり下げられます。

補正作業の基本フロー

本番作業では、1本のSQLをいきなり流すのではなく、段階を分けます。おすすめの流れは、事前確認、作業テーブル作成、バックアップ、更新、差分確認、コミット、作業記録です。

  1. 補正理由と対象条件を明文化する
  2. SELECTで対象件数と対象キーを確認する
  3. 対象行を作業テーブルへ固定する
  4. 補正前の値をバックアップする
  5. UPDATEを実行し、更新件数を検証する
  6. 補正後の差分を確認する
  7. 問題なければコミットする
  8. 作業ログ、実行SQL、確認結果を残す

ここで重要なのは、確認用SELECTと更新用UPDATEの条件を別々に書かないことです。確認時と更新時で条件がずれると事故の原因になります。そのため、対象キーを作業テーブルに固定し、以降の処理はその作業テーブルを基準に進めます。

また、ROLLBACKとロールバックSQLは分けて考えます。ROLLBACKはコミット前のトランザクションを取り消す操作です。一方、ロールバックSQLは、コミット後に問題が見つかった場合にバックアップから元の値へ戻すための補正SQLです。本番作業では、コミット前に戻す手段と、コミット後に戻す手段の両方を用意しておくと安全です。

作業IDを決めて証跡を残す

データ補正は、後から調べられるように作業IDを付けて管理します。チケット番号、障害管理番号、依頼番号などがある場合は、それをfix_idとして使うとよいです。作業テーブルやログテーブルにも同じIDを入れておけば、対象行、バックアップ、確認結果をひも付けられます。

data-fix-log-ddl.sql
CREATE TABLE data_fix_log (
  fix_id        VARCHAR2(50)  NOT NULL,
  step_name     VARCHAR2(50)  NOT NULL,
  status        VARCHAR2(20)  NOT NULL,
  row_count     NUMBER,
  note          VARCHAR2(1000),
  executed_by   VARCHAR2(128) DEFAULT SYS_CONTEXT('USERENV','SESSION_USER') NOT NULL,
  executed_at   TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
);

このようなログテーブルは、作業前に用意しておく運用基盤です。補正作業のたびにDDLを実行すると暗黙コミットが発生するため、本番作業当日にCREATE TABLEから始めるのは避けます。DDLの自動コミットを理解したうえで、実行スクリプトと準備スクリプトを分けましょう。ログ設計の考え方は DBMS_APPLICATION_INFOとログ設計 も参考になります。

UPDATE前に対象件数を確認する

最初にやるべきことは、対象条件をSELECTで確認することです。この段階ではまだ更新しません。対象キー、現在値、補正後の値、補正理由を見える形にして、作業依頼内容と一致しているか確認します。

precheck-target.sql
SELECT
  o.order_id,
  o.customer_id,
  o.order_status AS current_status,
  'CANCELLED'    AS new_status,
  o.updated_at
FROM orders o
WHERE o.order_status = 'TEMP_CANCEL'
  AND o.order_date >= DATE '2026-05-01'
  AND o.order_date <  DATE '2026-06-01'
  AND EXISTS (
    SELECT 1
    FROM support_ticket t
    WHERE t.order_id = o.order_id
      AND t.ticket_no = 'INC-20260602-001'
  )
ORDER BY o.order_id;

件数だけでなく、実際のキー一覧を見るのが大切です。COUNT(*)が想定件数と一致していても、対象の中身が違うことはあります。特に顧客ID、注文ID、締め状態、作成日、最終更新日などは、業務担当者が確認しやすい形で出力しておくとレビューしやすくなります。

対象行を作業テーブルに固定する

確認した対象は、作業テーブルに固定します。以降のバックアップ、更新、差分確認、ロールバックは、この作業テーブルを基準にします。これにより、確認時と実行時で対象条件がずれるリスクを減らせます。

data-fix-work-ddl.sql
CREATE TABLE data_fix_order_work (
  fix_id          VARCHAR2(50) NOT NULL,
  order_id        NUMBER       NOT NULL,
  old_status      VARCHAR2(30) NOT NULL,
  new_status      VARCHAR2(30) NOT NULL,
  reason          VARCHAR2(400),
  created_at      TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
  CONSTRAINT pk_data_fix_order_work PRIMARY KEY (fix_id, order_id)
);

このDDLも事前準備として実行します。本番作業当日は、既存の作業テーブルへ対象をINSERTするだけにしておくと、トランザクションの扱いが明確になります。

insert-work-target.sql
INSERT INTO data_fix_order_work (
  fix_id,
  order_id,
  old_status,
  new_status,
  reason
)
SELECT
  'INC-20260602-001' AS fix_id,
  o.order_id,
  o.order_status     AS old_status,
  'CANCELLED'        AS new_status,
  '誤ってTEMP_CANCELのまま残った注文ステータスを補正' AS reason
FROM orders o
WHERE o.order_status = 'TEMP_CANCEL'
  AND o.order_date >= DATE '2026-05-01'
  AND o.order_date <  DATE '2026-06-01'
  AND EXISTS (
    SELECT 1
    FROM support_ticket t
    WHERE t.order_id = o.order_id
      AND t.ticket_no = 'INC-20260602-001'
  );

SELECT COUNT(*) AS target_count
FROM data_fix_order_work
WHERE fix_id = 'INC-20260602-001';

ポイントは、作業テーブルにold_statusnew_statusの両方を持たせることです。これにより、更新時に現在値が本当にold_statusのままかを検証でき、ロールバックにも利用できます。

補正前データをバックアップする

更新前には、対象行の補正前データをバックアップします。ロールバックSQLを作るためだけでなく、作業後の問い合わせや監査にも使えます。推奨は、事前に作成済みのバックアップテーブルへ、作業時にINSERT INTO ... SELECTで対象行だけを退避する形です。テーブル単位のバックアップは Oracleで指定したテーブルのみバックアップを取る方法 でも解説しています。

backup-insert-pattern.sql
INSERT INTO data_fix_order_backup (
  fix_id,
  order_id,
  customer_id,
  order_status,
  order_date,
  updated_at,
  backed_up_at
)
SELECT
  'INC-20260602-001',
  o.order_id,
  o.customer_id,
  o.order_status,
  o.order_date,
  o.updated_at,
  SYSTIMESTAMP
FROM orders o
WHERE EXISTS (
  SELECT 1
  FROM data_fix_order_work w
  WHERE w.fix_id = 'INC-20260602-001'
    AND w.order_id = o.order_id
);

この方法なら、バックアップ取得もDMLとして扱えます。補正作業のトランザクション内で、バックアップ、更新、差分確認、必要ならROLLBACKという流れを取りやすくなります。

CREATE TABLE AS SELECTで対象行を退避する方法もありますが、CTASはDDLなので暗黙コミットが発生します。本番作業中に実行すると、直前のDMLまでコミットされる可能性があるため、通常は事前準備や検証環境向けの方法として扱います。

backup-ctas-preparation.sql
CREATE TABLE data_fix_order_backup AS
SELECT
  'INC-20260602-001' AS fix_id,
  o.*,
  SYSTIMESTAMP AS backed_up_at
FROM orders o
WHERE EXISTS (
  SELECT 1
  FROM data_fix_order_work w
  WHERE w.fix_id = 'INC-20260602-001'
    AND w.order_id = o.order_id
);

更新前後の差分を見える化する

補正作業では、更新前の状態、更新予定の値、更新後の値を並べて確認できるようにします。差分確認SQLを先に用意しておくと、コミット前の判断がしやすくなります。

diff-check-before.sql
SELECT
  w.order_id,
  w.old_status,
  w.new_status,
  o.order_status AS current_status,
  CASE
    WHEN o.order_status = w.old_status THEN 'OK'
    ELSE 'CHANGED_BEFORE_UPDATE'
  END AS check_result
FROM data_fix_order_work w
JOIN orders o
  ON o.order_id = w.order_id
WHERE w.fix_id = 'INC-20260602-001'
ORDER BY w.order_id;

CHANGED_BEFORE_UPDATEが出た場合は、確認後に別処理で対象行が変わった可能性があります。そのまま更新せず、対象条件を見直します。本番では、確認と更新の間に別トランザクションが動くことを前提に設計する必要があります。

排他制御をどう考えるか

対象行を作業テーブルに固定しても、実テーブル側の行が別セッションで更新される可能性は残ります。そのため、補正スクリプトでは現在値チェックを入れますが、影響が大きい作業では排他制御も検討します。たとえば、短時間のメンテナンス時間を確保する、アプリ側の更新を止める、または更新直前に対象行をSELECT ... FOR UPDATEでロックする方法があります。

lock-target-rows.sql
SELECT o.order_id
FROM orders o
WHERE EXISTS (
  SELECT 1
  FROM data_fix_order_work w
  WHERE w.fix_id = 'INC-20260602-001'
    AND w.order_id = o.order_id
)
FOR UPDATE NOWAIT;

FOR UPDATE NOWAITを使うと、他セッションが対象行をロックしている場合に待たずに失敗します。待機してよい作業ならWAIT n、待たずに中止したい作業ならNOWAITを選びます。ただし、長時間ロックを保持すると通常業務に影響するため、ロック取得後は更新、差分確認、コミットまたはロールバックまでを短時間で終える設計にします。

UPDATEは対象行と現在値を二重に絞る

更新時は、作業テーブルの対象キーだけでなく、現在値が補正前の値と一致していることも条件に入れます。これにより、作業テーブル作成後に別の処理が値を変更した行を誤って上書きしにくくなります。

safe-update.sql
UPDATE orders o
SET
  o.order_status = (
    SELECT w.new_status
    FROM data_fix_order_work w
    WHERE w.fix_id = 'INC-20260602-001'
      AND w.order_id = o.order_id
  ),
  o.updated_at = SYSTIMESTAMP,
  o.updated_by = 'DATA_FIX_INC_20260602_001'
WHERE EXISTS (
  SELECT 1
  FROM data_fix_order_work w
  WHERE w.fix_id = 'INC-20260602-001'
    AND w.order_id = o.order_id
    AND w.old_status = o.order_status
);

更新後は、SQL%ROWCOUNTまたは実行結果の件数を必ず確認します。想定件数と違う場合は、コミットせずに原因を確認します。SQL*PlusやSQLclで実行するなら、更新件数の表示を作業ログに残しておくとよいです。

PL/SQLで更新件数を検証する

更新件数の検証をSQLの実行結果だけに任せると、人の見落としが起きます。PL/SQLブロックにして、対象件数と更新件数が一致しない場合は例外にする方法も有効です。業務エラーコードとして管理する場合は PL/SQLの業務エラーコード設計 の考え方を組み合わせられます。

update-with-rowcount-check.sql
DECLARE
  v_fix_id         VARCHAR2(50) := 'INC-20260602-001';
  v_expected_count NUMBER;
  v_updated_count  NUMBER;
BEGIN
  SELECT COUNT(*)
    INTO v_expected_count
    FROM data_fix_order_work
   WHERE fix_id = v_fix_id;

  UPDATE orders o
     SET o.order_status = (
           SELECT w.new_status
             FROM data_fix_order_work w
            WHERE w.fix_id = v_fix_id
              AND w.order_id = o.order_id
         ),
         o.updated_at = SYSTIMESTAMP,
         o.updated_by = 'DATA_FIX_INC_20260602_001'
   WHERE EXISTS (
     SELECT 1
       FROM data_fix_order_work w
      WHERE w.fix_id = v_fix_id
        AND w.order_id = o.order_id
        AND w.old_status = o.order_status
   );

  v_updated_count := SQL%ROWCOUNT;

  IF v_updated_count <> v_expected_count THEN
    RAISE_APPLICATION_ERROR(
      -20991,
      '更新件数が想定と一致しません。expected='
      || v_expected_count || ', actual=' || v_updated_count
    );
  END IF;

  INSERT INTO data_fix_log(fix_id, step_name, status, row_count, note)
  VALUES (v_fix_id, 'UPDATE_ORDERS', 'OK', v_updated_count, '注文ステータス補正');

  -- 確認が終わるまでCOMMITしない
END;
/

この例では、PL/SQLブロック内ではCOMMITしていません。差分確認を実行して問題がなければ、作業者が明示的にコミットします。作業手順によってはブロック内でコミットする場合もありますが、その場合は失敗時の復旧手順をより厳密に用意します。

コミット前に補正後の差分を確認する

更新後は、補正後の値が作業テーブルのnew_statusと一致しているか確認します。この確認をコミット前に実施すれば、問題があった場合にロールバックできます。

diff-check-after.sql
SELECT
  w.order_id,
  w.old_status,
  w.new_status,
  o.order_status AS after_status,
  CASE
    WHEN o.order_status = w.new_status THEN 'OK'
    ELSE 'NG'
  END AS check_result
FROM data_fix_order_work w
JOIN orders o
  ON o.order_id = w.order_id
WHERE w.fix_id = 'INC-20260602-001'
ORDER BY w.order_id;

SELECT check_result, COUNT(*) AS rows_count
FROM (
  SELECT
    CASE
      WHEN o.order_status = w.new_status THEN 'OK'
      ELSE 'NG'
    END AS check_result
  FROM data_fix_order_work w
  JOIN orders o
    ON o.order_id = w.order_id
  WHERE w.fix_id = 'INC-20260602-001'
)
GROUP BY check_result;

差分確認では、全件一覧と集計の両方を用意すると便利です。件数が多い場合は一覧をCSVに出力し、レビュー用に保存します。CSV取込や検証の考え方は ステージングテーブル設計 とも相性がよいです。

ロールバックSQLを用意する

ロールバックは、単にROLLBACKするだけではありません。コミット前ならROLLBACKで戻せますが、コミット後に問題が見つかった場合は、バックアップテーブルを使って補正前の値へ戻すSQLが必要です。

rollback-from-backup.sql
UPDATE orders o
SET
  o.order_status = (
    SELECT b.order_status
    FROM data_fix_order_backup b
    WHERE b.fix_id = 'INC-20260602-001'
      AND b.order_id = o.order_id
  ),
  o.updated_at = SYSTIMESTAMP,
  o.updated_by = 'ROLLBACK_INC_20260602_001'
WHERE EXISTS (
  SELECT 1
  FROM data_fix_order_backup b
  WHERE b.fix_id = 'INC-20260602-001'
    AND b.order_id = o.order_id
);

ロールバックSQLも、実行前に対象件数を確認します。戻す対象が本当に今回の補正対象だけか、バックアップが重複していないか、戻す値がNULLを含むかを確認します。補正SQLと同じくらい、ロールバックSQLのレビューも重要です。

SAVEPOINTを使う場合の注意点

複数テーブルを順番に補正する場合は、SAVEPOINTを使って一部だけ戻せるようにする方法があります。ただし、DDLを実行すると暗黙コミットされるため、SAVEPOINTで戻せません。そのため、DDLは事前準備、DMLは本番作業、という分離が大切です。

savepoint-data-fix.sql
SAVEPOINT before_order_fix;

UPDATE orders
SET order_status = 'CANCELLED'
WHERE order_id IN (
  SELECT order_id
  FROM data_fix_order_work
  WHERE fix_id = 'INC-20260602-001'
);

-- 差分確認で問題があれば
ROLLBACK TO before_order_fix;

-- 問題がなければ最終的に
COMMIT;

SAVEPOINTは便利ですが、過信しないことが大切です。作業端末の切断、DDLの混入、別セッションでの確認、アプリ側の同時更新など、現場では想定外のことが起きます。必ず補正前バックアップとロールバックSQLを別に用意しておきましょう。

本番作業ログを残す

補正作業は、作業ログを残して初めて完了です。作業依頼番号、作業者、開始時刻、対象件数、更新件数、確認結果、コミット時刻を残します。バッチ作業として補正する場合は PL/SQLのジョブ実行履歴テーブル設計 のように、実行履歴を残す設計も有効です。

insert-data-fix-log.sql
INSERT INTO data_fix_log(
  fix_id,
  step_name,
  status,
  row_count,
  note
)
VALUES (
  'INC-20260602-001',
  'COMMIT',
  'OK',
  25,
  '注文ステータス補正をコミット。差分確認OK。'
);

COMMIT;

ログは、補正作業の正しさを証明する材料になります。実行SQL、確認SQLの結果、対象件数、承認者、作業日時を一緒に残しておくと、後日の問い合わせに強くなります。

外部連携データの補正では再送も考える

補正対象が外部API連携やキュー処理に関係している場合、DBの値を直すだけでは終わらないことがあります。補正後に再送が必要なのか、外部システム側の状態と整合しているのか、重複送信にならないかを確認します。再送キューを使う設計は アウトボックスパターンと再送キュー設計 と組み合わせて考えると安全です。

DBだけ直せばよいか外部システムに送信済みのデータなら、相手側の状態確認も必要です。
再送が必要か補正後にイベントや通知を再送する必要があるか確認します。
二重送信を防げるか再送キーや処理済みフラグがないと、同じ処理を重複実行するリスクがあります。
ログが追えるか補正前後と外部連携ログを同じ作業IDで追えるようにします。

やってはいけない補正スクリプト

最後に、避けるべきパターンを整理します。どれも現場で起きやすく、しかも事故につながりやすいものです。

WHERE条件が曖昧なUPDATEstatus = 'TEMP'だけのような条件は危険です。日付、依頼番号、対象キーで絞ります。
確認SELECTとUPDATE条件が違う確認時は10件、更新時は別条件で100件、という事故につながります。
バックアップなしで更新するコミット後に戻す値が分からなくなります。
DDLとDMLを混ぜる暗黙コミットにより、ROLLBACKできると思っていた作業が戻せなくなります。
更新件数を見ない0件更新も大量更新も、コミット後に気づくと復旧が難しくなります。
作業ログを残さない後から説明できず、同じ調査を繰り返すことになります。

本番作業チェックリスト

本番反映前には、次の観点でレビューします。チェックリストにしておくと、作業者による品質のばらつきを減らせます。

  • 補正理由と作業IDが明確になっている
  • 対象件数と対象キー一覧を確認済み
  • 作業テーブルで対象行を固定している
  • 補正前データのバックアップを取得している
  • UPDATE条件に対象キーと現在値チェックが入っている
  • 更新件数の期待値を定義している
  • 差分確認SQLを用意している
  • コミット前に確認する手順になっている
  • コミット後のロールバックSQLを用意している
  • 作業ログと証跡の保存先が決まっている

まとめ

本番データ補正スクリプトは、UPDATE文の書き方だけで安全になるものではありません。対象行を固定し、補正前データを残し、更新件数を検証し、差分を確認し、ロールバック手順と作業ログを用意することで、初めて実務で使える補正作業になります。

特に重要なのは、確認用SELECTと更新用UPDATEを同じ対象セットで扱うこと、DDLとDMLを分けること、コミット前後の戻し方を両方考えることです。補正作業は一度きりに見えて、設計の良し悪しが運用品質に直結します。小さな補正でも、手順化して安全に進めましょう。