システム移行計画書とは、旧システムから新システムへ「何を・いつ・だれが・どの順番で」移し、うまくいかないときに「いつ・だれの判断で」元に戻すかを、関係者全員で合意するための文書です。 移行は一度きりの作業になりやすいため、手順だけでなく、判定の時刻と基準を前もって決めておくことが大切です。

この記事では、移行方式(一括・段階・並行稼働)の比較表、移行計画書の目次テンプレート、そして架空の「社内備品管理システム」を例にした記入例(RACI体制表・スケジュール・移行対象一覧・リハーサル計画・当日のタイムチャート・Go/No-Goと切り戻し判定表)を載せます。さらに、PostgreSQL 16.15 で旧テーブルから新テーブルへ移行SQLを実行し、件数・合計値・md5ハッシュで一致を確かめる検証SQLと、その実行結果(不一致を検出した例を含む)も掲載しています。

移行計画書に書く項目の早見表

章 書くこと 決めておく数字・名前の例
1. 目的と範囲 なぜ移行するか、対象と対象外 対象システム、対象外の機能
2. 移行方式 一括・段階・並行稼働のどれか、選んだ理由 業務停止できる時間(例:土日の36時間)
3. 体制(RACI) 作業ごとの実行者・最終責任者・相談先・報告先 判定者の氏名と連絡手段
4. スケジュール 準備からリハーサル、本番、安定化まで リハーサル2回、本番日、予備日
5. 移行対象一覧 データ・プログラム・設定・利用者・帳票 テーブル数、件数、移行方法
6. データ移行手順 抽出・変換・投入・検証の順番と担当 変換ルール、所要時間の実測値
7. 移行検証 件数・合計値・ハッシュの突き合わせ、動作確認 一致の条件、確認する画面
8. リハーサル計画 何回、何を確認するか、合格の条件 所要時間の上限、手順書の修正
9. 当日のタイムチャート 時間単位の作業、担当、判定ポイント 開始・終了・判定時刻
10. Go/No-Goと切り戻し 判定時刻、判定者、条件、切り戻し手順 「この時刻までに検証OKでなければ戻す」
11. リスクと対策 起こりうることと、前もって打つ手 発生時の連絡先
12. 連絡・周知 利用者への告知、当日の連絡ルート 告知日、問い合わせ窓口

特に「判定時刻・判定者・条件」の3点を数字と名前で書いておくと、当日に迷わず判断できます。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「システム移行計画書の書き方|目次テンプレートと切り戻し判断基準」

移行方式の比較:一括・段階・並行稼働

移行方式は「業務を止められる時間」と「旧システムをいつまで動かせるか」で選ぶのが基本です。

方式 進め方 向いている場合 気をつけること 切り戻しのしやすさ
一括移行 決めた日に全部を切り替える 業務を止められる時間があり、データ量が時間内に収まる 当日の作業量が多い。リハーサルで所要時間を測る 旧システムを残しておけば戻しやすい
段階移行 機能・拠点・利用者のグループごとに順番に切り替える 利用者が多い、機能ごとに独立している 移行期間中は新旧の連携が必要 グループ単位で戻せる
並行稼働 新旧を同時に動かし、結果を比べてから旧を止める 計算結果を新旧で比べたい 二重入力や同期の手間がかかる 旧が動いているので戻しやすい
パイロット移行 一部の利用者で先に使い、問題がなければ全体へ 操作の変化が大きい 先行利用者の支援が必要 先行範囲だけ戻せばよい

どの方式でも、移行計画書には「選んだ理由」を1〜2行で書いておくと、後から見た人が前提を理解できます。

移行計画書の目次テンプレート

そのままコピーして使える目次です。各章に何を書くかを <!-- --> で添えています。

# {システム名} 移行計画書

版:1.0({作成日}) 作成:{担当}

## 1. 目的と範囲
<!-- 移行の目的、対象システム、対象外、前提条件 -->
## 2. 移行方式
<!-- 一括/段階/並行稼働、選んだ理由、停止できる時間 -->
## 3. 体制と役割(RACI)
<!-- 作業ごとの R/A/C/I、判定者、連絡先 -->
## 4. スケジュール
<!-- 準備、リハーサル、本番、安定化、旧システム停止 -->
## 5. 移行対象一覧
<!-- データ、プログラム、設定、アカウント、外部連携、帳票 -->
## 6. データ移行手順
<!-- 抽出、変換ルール、投入、検証、所要時間 -->
## 7. 移行検証
<!-- 件数・合計値・ハッシュ、画面での確認、合格条件 -->
## 8. リハーサル計画
<!-- 回数、確認内容、合格条件、手順書への反映 -->
## 9. 当日のタイムチャート
<!-- 時刻、作業、担当、判定ポイント -->
## 10. Go/No-Go 判定と切り戻し
<!-- 判定時刻、判定者、条件、切り戻し手順と所要時間 -->
## 11. リスクと対策
## 12. 連絡・周知
<!-- 利用者への告知、当日の連絡ルート、問い合わせ窓口 -->
## 13. 移行後の安定化と旧システムの停止
<!-- 監視強化の期間、旧システムのデータ保管期間 -->
## 付録
<!-- 作業手順書、検証SQL、連絡先一覧 -->

当日の細かいコマンド手順は「作業手順書」として別にし、移行計画書は「全体の段取りと判断の基準」に絞ると、読みやすさと更新のしやすさが両立します。リリース作業の手順書の書き方は「リリース手順書の書き方|切り戻し付きテンプレートと判定表」の記事で解説しています。

記入例:社内備品管理システムの移行

ここからは、架空の「社内備品管理システム」を例に、主な章を埋めていきます。

  • 旧システム:社内サーバー上の業務アプリ(データベースはPostgreSQL)
  • 新システム:クラウド上の新しい備品管理アプリ(データベースはPostgreSQL)
  • 方式:一括移行(理由:利用者は全社員だが操作は単純で、土日の36時間で移行と検証が終わることをリハーサルで確認できる見込みのため)
  • 停止時間:金曜18:00〜月曜6:00(予備を含む)

3. 体制と役割(RACI)

RACIは、作業ごとに R(実行する人)、A(最終責任者。1人だけ)、C(事前に相談する人)、I(結果を報告する人)を割り当てる表です。

作業 移行責任者 移行リーダー DB担当 アプリ担当 利用部門代表 運用担当
移行計画の作成・承認 A R C C C C
データ抽出・変換・投入 I A R C - I
データ移行検証 I A R R - I
画面での動作確認 I A - R R I
Go/No-Go判定 A R C C C I
切り戻し作業 A R R R I C
利用者への周知 I A - - R I

判定者(A)は氏名と当日の連絡手段まで書き、不在時の代理も決めておくと、判断が止まりません。

4. スケジュール

時期 作業 完了の条件
本番の8週前 移行計画書の承認、移行対象の確定 移行対象一覧に担当と方法が埋まっている
6週前 移行SQL・検証SQLの作成 開発環境で検証SQLが全項目OK
4週前 リハーサル1回目 手順と所要時間を記録、課題を一覧化
3週前 利用者への告知(停止日時、新しいURL) 告知が全社に届いている
2週前 リハーサル2回目(本番と同じ手順・同じ件数) 所要時間が計画内、検証が全項目OK
1週前 最終Go/No-Go判定会議(本番を実施するか) 判定者の承認
当日 本番移行 タイムチャートのとおり
移行後2週間 安定化(問い合わせ対応、監視強化) 重大な問い合わせが落ち着いている
移行後3か月 旧システム停止、データの保管 保管期間と保管場所を記録

5. 移行対象一覧

No 種類 対象 件数・量 移行方法 担当 備考
1 データ 備品(old.equipment) 10,000件 SQLで変換して投入 DB担当 名前の前後の空白を除去、日付不明は NULL
2 データ 貸出履歴(old.loan) 50,000件 SQLで変換して投入 DB担当 備品コードを大文字にそろえて紐付け
3 マスタ 備品分類 3件 新システムで初期登録 アプリ担当 分類コードは旧と同じ値
4 アカウント 利用者 全社員 社内の認証基盤と連携 アプリ担当 旧パスワードは移行しない
5 設定 通知メールの宛先 一式 手作業で登録 運用担当 登録後に送信テスト
6 外部連携 資産台帳への月次出力 1本 新システムで作り直し アプリ担当 旧と同じ形式で出力を比較
7 対象外 5年以上前の貸出履歴 - 移行しない(旧データを保管) 移行リーダー 利用部門と合意済み

「移行しないもの」も一覧に書くと、移行後に「あのデータはどこ?」という問い合わせに答えやすくなります。

データ移行手順と検証(PostgreSQLで実行)

ここでは、データ移行の中心となる「変換して投入し、一致を確かめる」部分を、PostgreSQL 16.15 で実際に動かした結果とともに示します。旧テーブル(old スキーマ)と新テーブル(new スキーマ)を1つのデータベースに用意していますが、別サーバーの場合も、検証SQLの考え方は同じです。

旧テーブルと新テーブル

-- 旧システム(移行元)
CREATE SCHEMA old;
CREATE TABLE old.equipment (
  eq_cd       varchar(10) PRIMARY KEY,   -- 備品コード(例 EQ00001)
  eq_name     varchar(100) NOT NULL,     -- 前後に空白が入っていることがある
  category_cd char(2)      NOT NULL,     -- 01:PC 02:モニター 03:家具
  price       numeric(10)  NOT NULL,     -- 購入金額(円)
  purchased   char(8)      NOT NULL,     -- YYYYMMDD。不明は '00000000'
  del_flg     char(1)      NOT NULL      -- '0':有効 '1':削除済み
);
CREATE TABLE old.loan (
  loan_no    integer PRIMARY KEY,
  eq_cd      varchar(10) NOT NULL,       -- 一部が小文字で入っている
  emp_cd     varchar(8)  NOT NULL,
  loan_ymd   char(8)     NOT NULL,
  return_ymd varchar(8)  NOT NULL        -- 未返却は ''
);

-- 新システム(移行先)
CREATE SCHEMA new;
CREATE TABLE new.category (code text PRIMARY KEY, name text NOT NULL);
INSERT INTO new.category VALUES ('01', 'PC'), ('02', 'モニター'), ('03', '家具');
CREATE TABLE new.equipment (
  id             bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  code           text NOT NULL UNIQUE,
  name           text NOT NULL,
  category_code  text NOT NULL REFERENCES new.category(code),
  purchase_price numeric(12) NOT NULL CHECK (purchase_price >= 0),
  purchased_on   date,                   -- 不明は NULL
  is_deleted     boolean NOT NULL
);
CREATE TABLE new.loan (
  id            bigint PRIMARY KEY,      -- 旧 loan_no を引き継ぐ
  equipment_id  bigint NOT NULL REFERENCES new.equipment(id),
  employee_code text NOT NULL,
  loaned_on     date NOT NULL,
  returned_on   date                     -- 未返却は NULL
);

テストデータは generate_series で備品10,000件・貸出50,000件を作りました。50件に1件は名前の前後に空白、97件に1件は購入日 '00000000'、貸出の40件に1件は備品コードが小文字、7件に1件は未返却('')という、移行でつまずきやすい形をわざと混ぜています。

INSERT INTO old.equipment
SELECT 'EQ' || lpad(g::text, 5, '0'),
       CASE WHEN g % 50 = 0 THEN ' ノートPC ' || g || ' ' ELSE '備品' || g END,
       lpad((g % 3 + 1)::text, 2, '0'),
       (g % 200 + 1) * 1000,
       CASE WHEN g % 97 = 0 THEN '00000000'
            ELSE to_char(date '2020-01-01' + (g % 2000), 'YYYYMMDD') END,
       CASE WHEN g % 20 = 0 THEN '1' ELSE '0' END
FROM generate_series(1, 10000) AS g;

INSERT INTO old.loan
SELECT g,
       CASE WHEN g % 40 = 0 THEN lower('EQ' || lpad((g % 10000 + 1)::text, 5, '0'))
            ELSE 'EQ' || lpad((g % 10000 + 1)::text, 5, '0') END,
       'E' || lpad((g % 300)::text, 5, '0'),
       to_char(date '2024-01-01' + (g % 600), 'YYYYMMDD'),
       CASE WHEN g % 7 = 0 THEN ''
            ELSE to_char(date '2024-01-01' + (g % 600) + 14, 'YYYYMMDD') END
FROM generate_series(1, 50000) AS g;

移行SQL(03_migrate.sql)

変換ルールは移行計画書の「6. データ移行手順」に表で書き、SQLはその表どおりに作ると、レビューしやすくなります。

旧の列 新の列 変換ルール
eq_name name 前後の空白を除去(btrim)
purchased purchased_on '00000000' は NULL、それ以外は日付に変換
del_flg is_deleted '1' を true
loan.eq_cd equipment_id 大文字にそろえ、前後の空白を除いて備品コードで紐付け
return_ymd returned_on '' は NULL
BEGIN;
INSERT INTO new.equipment (code, name, category_code, purchase_price, purchased_on, is_deleted)
SELECT eq_cd,
       btrim(eq_name),
       category_cd,
       price,
       CASE WHEN purchased = '00000000' THEN NULL ELSE to_date(purchased, 'YYYYMMDD') END,
       del_flg = '1'
FROM old.equipment
ORDER BY eq_cd;

INSERT INTO new.loan (id, equipment_id, employee_code, loaned_on, returned_on)
SELECT l.loan_no,
       e.id,
       l.emp_cd,
       to_date(l.loan_ymd, 'YYYYMMDD'),
       CASE WHEN l.return_ymd = '' THEN NULL ELSE to_date(l.return_ymd, 'YYYYMMDD') END
FROM old.loan AS l
JOIN new.equipment AS e ON e.code = upper(btrim(l.eq_cd));
COMMIT;

検証SQL(04_verify.sql):件数・合計値・ハッシュで突き合わせる

件数だけでは「中身の変化」を見落とすことがあるため、合計値と全列のハッシュも比べます。 ハッシュは、各行を | でつないだ文字列を主キー順に並べて連結し、md5 をとったものです(並び順が違うと一致しないため、両側で同じキー順にします)。旧側は「変換ルールを当てはめた後の期待値」として計算します。

WITH chk(no, item, old_value, new_value) AS (
  SELECT 1, '備品 件数',
         (SELECT count(*) FROM old.equipment)::text,
         (SELECT count(*) FROM new.equipment)::text
  UNION ALL
  SELECT 2, '備品 購入金額合計',
         (SELECT sum(price) FROM old.equipment)::text,
         (SELECT sum(purchase_price) FROM new.equipment)::text
  UNION ALL
  SELECT 3, '備品 有効件数',
         (SELECT count(*) FROM old.equipment WHERE del_flg = '0')::text,
         (SELECT count(*) FROM new.equipment WHERE NOT is_deleted)::text
  UNION ALL
  SELECT 4, '備品 全列ハッシュ',
         (SELECT md5(string_agg(concat_ws('|', eq_cd, btrim(eq_name), category_cd, price,
                                          coalesce(nullif(purchased, '00000000'), ''),
                                          del_flg = '1'),
                                E'\n' ORDER BY eq_cd))
            FROM old.equipment),
         (SELECT md5(string_agg(concat_ws('|', code, name, category_code, purchase_price,
                                          coalesce(to_char(purchased_on, 'YYYYMMDD'), ''),
                                          is_deleted),
                                E'\n' ORDER BY code))
            FROM new.equipment)
  UNION ALL
  SELECT 5, '貸出 件数',
         (SELECT count(*) FROM old.loan)::text,
         (SELECT count(*) FROM new.loan)::text
  UNION ALL
  SELECT 6, '貸出 未返却件数',
         (SELECT count(*) FROM old.loan WHERE return_ymd = '')::text,
         (SELECT count(*) FROM new.loan WHERE returned_on IS NULL)::text
  UNION ALL
  SELECT 7, '貸出 全列ハッシュ',
         (SELECT md5(string_agg(concat_ws('|', loan_no, upper(btrim(eq_cd)), emp_cd, loan_ymd, return_ymd),
                                E'\n' ORDER BY loan_no))
            FROM old.loan),
         (SELECT md5(string_agg(concat_ws('|', l.id, e.code, l.employee_code,
                                          to_char(l.loaned_on, 'YYYYMMDD'),
                                          coalesce(to_char(l.returned_on, 'YYYYMMDD'), '')),
                                E'\n' ORDER BY l.id))
            FROM new.loan AS l JOIN new.equipment AS e ON e.id = l.equipment_id)
)
SELECT no, item, old_value, new_value,
       CASE WHEN old_value IS NOT DISTINCT FROM new_value THEN 'OK' ELSE 'NG' END AS result
FROM chk
ORDER BY no;

実行結果1:紐付けで大文字・小文字をそろえなかった場合

最初に、貸出の紐付けを ON e.code = l.eq_cd(大文字にそろえない)にして実行した結果です。

BEGIN
INSERT 0 10000
INSERT 0 48750
COMMIT
 no |       item        |            old_value             |            new_value             | result
----+-------------------+----------------------------------+----------------------------------+--------
  1 | 備品 件数         | 10000                            | 10000                            | OK
  2 | 備品 購入金額合計 | 1005000000                       | 1005000000                       | OK
  3 | 備品 有効件数     | 9500                             | 9500                             | OK
  4 | 備品 全列ハッシュ | 49fa1437f3ae05bfc445ac16a824c9c2 | 49fa1437f3ae05bfc445ac16a824c9c2 | OK
  5 | 貸出 件数         | 50000                            | 48750                            | NG
  6 | 貸出 未返却件数   | 7142                             | 6964                             | NG
  7 | 貸出 全列ハッシュ | 97ce7d630ec24f991535529b186215c6 | b5c1f43eb5c233636043d34946694ea5 | NG
(7 rows)

SQLはエラーにならずに完了し、1,250件が静かに抜け落ちました。 JOIN(内部結合)は相手が見つからない行を結果に含めないためです。差分を調べるSQLで原因がすぐわかります。

SELECT l.loan_no, l.eq_cd
FROM old.loan AS l
WHERE NOT EXISTS (SELECT 1 FROM new.loan AS n WHERE n.id = l.loan_no)
ORDER BY l.loan_no
LIMIT 3;
 loan_no |  eq_cd
---------+---------
      40 | eq00041
      80 | eq00081
     120 | eq00121
(3 rows)

抜けた1,250件はすべて備品コードが小文字の行でした。upper(btrim(l.eq_cd)) で紐付けると解消します。

実行結果2:件数は合っているのに中身が違う場合

次は、紐付けは直したうえで、名前の空白除去(btrim)を入れ忘れた場合です。

 no |       item        |            old_value             |            new_value             | result
----+-------------------+----------------------------------+----------------------------------+--------
  1 | 備品 件数         | 10000                            | 10000                            | OK
  2 | 備品 購入金額合計 | 1005000000                       | 1005000000                       | OK
  3 | 備品 有効件数     | 9500                             | 9500                             | OK
  4 | 備品 全列ハッシュ | 49fa1437f3ae05bfc445ac16a824c9c2 | 5d4176c42ed46cf30d21ce7f4bc9aa41 | NG
  5 | 貸出 件数         | 50000                            | 50000                            | OK
  6 | 貸出 未返却件数   | 7142                             | 7142                             | OK
  7 | 貸出 全列ハッシュ | 97ce7d630ec24f991535529b186215c6 | 97ce7d630ec24f991535529b186215c6 | OK
(7 rows)

件数と合計値はすべてOKで、ハッシュだけがNGになりました。 これが、ハッシュでの突き合わせを入れる理由です。

実行結果3:正しい移行SQL(所要時間つき)

Timing is on.
BEGIN
Time: 0.393 ms
INSERT 0 10000
Time: 126.636 ms
INSERT 0 50000
Time: 612.183 ms
COMMIT
Time: 123.922 ms
 no |       item        |            old_value             |            new_value             | result
----+-------------------+----------------------------------+----------------------------------+--------
  1 | 備品 件数         | 10000                            | 10000                            | OK
  2 | 備品 購入金額合計 | 1005000000                       | 1005000000                       | OK
  3 | 備品 有効件数     | 9500                             | 9500                             | OK
  4 | 備品 全列ハッシュ | 49fa1437f3ae05bfc445ac16a824c9c2 | 49fa1437f3ae05bfc445ac16a824c9c2 | OK
  5 | 貸出 件数         | 50000                            | 50000                            | OK
  6 | 貸出 未返却件数   | 7142                             | 7142                             | OK
  7 | 貸出 全列ハッシュ | 97ce7d630ec24f991535529b186215c6 | 97ce7d630ec24f991535529b186215c6 | OK
(7 rows)

Time: 155.025 ms

全項目OKになりました。 この検証環境では投入からCOMMITまでが約0.9秒、検証SQLが約0.2秒でした。本番の件数とサーバーでの所要時間はリハーサルで測り、タイムチャートの時間に反映します。

判定を自動化するシェルスクリプト(verify.sh)

検証結果にNGが1件でもあれば終了コード1を返すようにしておくと、当日の判定が明確になります。

#!/usr/bin/env bash
# 使い方: ./verify.sh {DB名}   (NG が1件でもあれば終了コード 1)
set -euo pipefail
DB="$1"
psql -d "$DB" -X -q -f 04_verify.sql
NG=$(psql -d "$DB" -X -At -f 04_verify.sql | awk -F'|' '$5 == "NG"' | wc -l)
if [ "$NG" -eq 0 ]; then
  echo "判定: OK(全項目一致)"
else
  echo "判定: NG(不一致 ${NG} 件)"
  exit 1
fi

新側の購入金額を1件だけ1円変えて実行すると、次のように検出されました(表の部分は抜粋)。

  2 | 備品 購入金額合計 | 1005000000                       | 1005000001                       | NG
  4 | 備品 全列ハッシュ | 49fa1437f3ae05bfc445ac16a824c9c2 | a54160b8470ed7c0bcd6780ce53b04c8 | NG
判定: NG(不一致 2 件)

終了コードは 1 でした。データベースの権限設計は「PostgreSQLのロールと権限|GRANT設計テンプレートとエラー対処」の記事も参考になります。

リハーサル計画

リハーサルは「本番と同じ手順・同じ件数・同じ時間の測り方」で行い、毎回の結果を手順書に戻すのがポイントです。

回 目的 使うデータ 合格の条件
1回目 手順の抜けと所要時間を把握する 本番の複製(個人情報は必要に応じてマスキング) 全手順を最後まで実施、課題一覧を作成
2回目 本番と同じ時刻表で通しで行う 本番の最新の複製 所要時間が計画内、検証SQLが全項目OK、切り戻し手順も1回実施

切り戻し手順もリハーサルで一度実際に行い、所要時間を測っておくと、判定時刻を逆算できます。

当日のタイムチャート(記入例)

金曜18:00に旧システムを停止し、日曜18:00までに完了する計画です。所要時間はリハーサル2回目の実測に余裕を加えた値として書きます。

時刻 作業 担当 判定・確認
金 17:30 作業開始の連絡、体制の確認 移行リーダー 全員の参加を確認
金 18:00 旧システムを利用停止(メンテナンス画面に切り替え) 運用担当 ログインできないことを確認
金 18:30 旧データベースのバックアップ取得 DB担当 バックアップの完了とサイズを確認
金 19:30 データ抽出・変換・投入(03_migrate.sql) DB担当 投入件数を記録
金 21:00 検証SQL(verify.sh) DB担当 全項目OK
金 21:30 判定1:データ移行のGo/No-Go 移行責任者 下の判定表
金 22:00 外部連携・通知メールの設定 アプリ担当・運用担当 送信テスト
土 9:00 画面での動作確認(確認シナリオ30件) アプリ担当・利用部門代表 結果を記録
土 15:00 判定2:本番切り替えのGo/No-Go(切り戻し判断の最終時刻) 移行責任者 下の判定表
土 16:00 新システムのURLを公開、利用者への案内 利用部門代表 案内の送信
日 10:00 監視・ログの確認 運用担当 エラーの有無
日 18:00 移行完了の報告 移行リーダー 関係者へ報告
月 9:00〜 問い合わせ窓口を強化 利用部門代表・運用担当 問い合わせを記録

Go/No-Go判定と切り戻し判断の基準

Go/No-Go判定とは、決めた時刻に「次へ進む(Go)か、止めて切り戻す(No-Go)か」を、決めた人が決めた条件で判断することです。条件は「だれが見ても同じ結論になる」書き方にします。

判定 判定時刻 判定者 Goの条件(すべて満たす) No-Goのときの対応
判定1 データ移行 金 21:30 移行責任者(不在時は移行リーダー) verify.sh が「判定: OK」/投入時のエラー0件/作業の遅れが60分以内 原因が30分以内に特定・修正できれば再投入して再判定(1回まで)、できなければ切り戻し
判定2 本番切り替え 土 15:00 移行責任者 確認シナリオ30件中、重大な不具合0件/軽微な不具合は回避策と修正予定日が決まっている/外部連携の送信テストOK 切り戻し
最終の切り戻し期限 土 15:00 移行責任者 この時刻を過ぎたら切り戻しは行わず、新システムで修正して対応する -

最終の切り戻し期限は「切り戻しにかかる時間(リハーサルの実測)+余裕」を業務再開の時刻から逆算して決めます。 この例では、切り戻しに約4時間、確認に2時間かかる想定で、月曜の業務開始までに十分な余裕をとって土曜15:00としています。

切り戻し手順(概要)

順番 作業 担当 確認
1 切り戻しの決定を関係者に連絡 移行リーダー 連絡の到達
2 新システムへのアクセスを止める 運用担当 新URLにつながらない
3 旧システムの設定を元に戻す(データは停止時点のまま) 運用担当 旧の画面が開く
4 旧システムの動作確認(主要シナリオ5件) 利用部門代表 全件OK
5 利用者へ「旧システムで再開」を案内 利用部門代表 案内の送信
6 原因の整理と次回の移行日の再設定 移行リーダー 課題一覧に記録

旧システムのデータを停止時点から変更しない運用にしておくと、切り戻しが「設定を戻すだけ」になり、短時間で済みます。

リスクと対策

リスク 起きること 前もって打つ手 起きたときの対応
データの形式が想定と違う 投入エラー、または行が抜け落ちる リハーサルで本番の複製を使う、検証SQLで件数・合計・ハッシュを比べる 差分SQLで対象行を特定して変換ルールを修正
所要時間が計画を超える 判定時刻に間に合わない リハーサルで実測し、余裕を持たせる。大量データは事前に移して当日は差分のみ 判定1で遅れが60分を超えたら切り戻し
外部連携の相手側が未対応 連携データが届かない 相手側と移行日を合意、テスト送信の日程を決める 旧形式で一時出力する手順を用意
利用者が新しい操作に迷う 問い合わせが集中する 事前の案内、よくある質問の用意 窓口の強化期間を設ける

よくある失敗を防ぐチェックリスト

確認すること チェック
移行対象一覧に「移行しないもの」も書いた □
変換ルールを表にし、SQLと対応づけた □
検証は件数だけでなく、合計値とハッシュも比べる □
内部結合(JOIN)で行が抜け落ちないことを件数で確認した □
リハーサルを本番と同じ件数・同じ手順で行い、所要時間を測った □
切り戻し手順もリハーサルで一度実施した □
判定時刻・判定者(代理を含む)・条件を数字と名前で書いた □
最終の切り戻し期限を、切り戻しの所要時間から逆算した □
旧システムのバックアップと保管期間を決めた □
利用者への告知日・問い合わせ窓口を決めた □
移行後の監視強化の期間と、旧システムの停止日を決めた □

移行後の監視や問い合わせ対応を含む、日々の運用の決め方は「運用設計書の書き方|項目一覧と記入例付きテンプレート」の記事にまとめています。

よくある質問

Q. 移行計画書と移行手順書の違いは何ですか? A. 移行計画書は「方式・体制・スケジュール・判定基準」といった全体の段取りと合意事項を書く文書で、移行手順書は当日のコマンドや画面操作を順番に書く作業用の文書です。計画書から手順書を参照する形にすると、それぞれを更新しやすくなります。

Q. データ移行の検証は件数の一致だけで十分ですか? A. 件数が一致しても、空白の除去漏れや値の変換違いは見つけられません。この記事の実行結果2のように、件数と合計値がOKでもハッシュでだけ差が見つかることがあります。件数・合計値・ハッシュの3つを組み合わせるのがおすすめです。

Q. 切り戻しの判断時刻はどう決めればよいですか? A. 「業務を再開する時刻」から「切り戻しにかかる時間(リハーサルで実測)」と「確認の時間」と「余裕」を引いて決めます。判定時刻を過ぎたら新システムで対応を続ける、という線も合わせて決めておくと、当日の判断が明確になります。

まとめ

  • 移行計画書は「何を・いつ・だれが・どの順番で」と「いつ・だれの判断で戻すか」を合意する文書
  • 方式は停止できる時間と旧システムを動かせる期間で、一括・段階・並行稼働から選ぶ
  • 体制はRACI表で、判定者(A)と代理を名前で決める
  • 当日のタイムチャートに判定時刻を入れ、Go/No-Goの条件を数字で書く
  • 最終の切り戻し期限は切り戻しの所要時間から逆算する
  • データ検証は件数・合計値・md5ハッシュの3つで突き合わせると、抜け落ちも中身の違いも見つかる
  • リハーサルは本番と同じ件数・手順で行い、切り戻しも一度試す

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「システム移行計画書の書き方|目次テンプレートと切り戻し判断基準」

株式会社RJCでは、システム移行の計画と検証の進め方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?