ふくふくHukuhuku Inc.
EP.06Legacy Revival 14分公開: 2026-09-01

履歴データをどう移すか ── 初期移行とスキーマ変換の設計

並行運用の前に、過去のデータを移す作業があります。何年ぶんを移すのか、壊れた古いデータをどう扱うのか。移行そのものより、判断のほうが難しい領域です。

#レガシー#データ移行#設計#運用
執筆 / 監修
松尾 亮合同会社ふくふく 代表社員

データ基盤・データパイプライン構築 / BI / 生成 AI 活用支援を専門とするエンジニア (28 年)。 本記事は AI 利用ポリシーに基づき、生成 AI の補助で執筆 → 人間が監修・編集して公開しています。

プロフィール詳細
シェア

EP.05 では、機能を少しずつ新側に移す並行運用を扱いました。この回はその前段、過去のデータをどう移すかを扱います。

地味な工程に見えますが、移行計画が破綻する原因の多くはここにあります。技術的な難しさより、判断の難しさが支配的な領域です。

1. まず「どこまで移すか」を決める

全件移すのが最も安全に見えます。しかし古いデータほど壊れている確率が高く、変換の手間も検証の手間も増えます。線を引く必要があります。

移す/移さないの判断
データ判断理由
直近1〜2年の取引移す日常的に参照される
現在有効な契約・顧客移す業務が止まる
法令で保存義務のある期間移す選択の余地がない
それより古い取引履歴参照専用に退避見ることはあるが、更新しない
削除済み・無効化済み移さない移す理由がない
用途不明のテーブル保留消す前に、誰が使っているか確認する

4行目の「参照専用に退避」が実務的な落としどころです。新システムには入れないが、必要になったら見られる場所に置く。移行の対象を絞れるうえ、捨てる判断も先送りできます。この形は EP.10 で扱う のときにも使うので、移行と退役で同じ仕組みを流用できるという利点もあります。

「念のため全部」の代償

全件を移すと、壊れたデータの変換規則を全部作ることになります。10年前の異常値のために例外処理を書き、それが新システムに恒久的に残る。古い歪みを新しい器に持ち込むのが、最も避けたい結果です。

2. データの実態を調べる

設計書に書かれた形式と、実際に入っている値は違います。移行の前に、実データを機械的に調べます。

移行前に必ず確認すること
SQL
-- 1. 想定外の値が入っていないか(区分値の実態)SELECT status, COUNT(*) AS 件数, MIN(created_at) AS 最古, MAX(created_at) AS 最新FROM legacy_ordersGROUP BY statusORDER BY 件数 DESC;
-- 2. 時期によって値の使い方が変わっていないかSELECT    DATE_TRUNC('year', created_at) AS,    COUNT(*)                                          AS 件数,    COUNT(*) FILTER (WHERE tax_rate IS NULL)          AS 税率なし,    COUNT(DISTINCT tax_rate)                          AS 税率の種類,    COUNT(*) FILTER (WHERE amount < 0)                AS マイナス金額FROM legacy_ordersGROUP BY 1ORDER BY 1;
-- 3. 参照先が存在しない行(壊れた関連)SELECT COUNT(*) AS 孤児レコードFROM legacy_orders oLEFT JOIN legacy_customers c ON c.id = o.customer_idWHERE c.id IS NULL;

2番目のクエリが特に重要です。年ごとに集計すると、いつ何が変わったかが見えます。ある年から税率の種類が増えている、ある時期だけ NULL が多い ── こうした断層が、変換規則を時期ごとに分ける必要があるかどうかを教えてくれます。

3番目の「孤児レコード」も必ず確認します。参照先が消えている行は、そのまま移すと新システムの制約に引っかかります。件数が少なければ個別対応、多ければ設計の見直しが要ります。

3. 変換規則をコードで表す

調べた結果を、変換規則としてコードに落とします。ここで大事なのは、変換できなかったものを黙って捨てないことです。

変換できないものを必ず記録する
Python
from dataclasses import dataclass, field

@dataclassclass MigrationResult:    converted: list = field(default_factory=list)    rejected: list = field(default_factory=list)   # 捨てずに記録する

# 時期によって区分の意味が変わっているので、期間ごとに規則を分けるSTATUS_MAP_OLD = {"1": "pending", "2": "paid", "3": "cancelled"}STATUS_MAP_NEW = {"P": "pending", "D": "paid", "C": "cancelled", "R": "refunded"}BOUNDARY = "2020-04-01"

def convert(row) -> tuple:    """(変換後, エラー理由) を返す。変換できなければ変換後は None。"""    table = STATUS_MAP_OLD if row["created_at"] < BOUNDARY else STATUS_MAP_NEW    status = table.get(row["status"])    if status is None:        return None, f"未知の区分値: {row['status']!r}{row['created_at']})"
    if row["amount"] is None:        return None, "金額が空"
    return {"id": row["id"], "status": status, "amount": row["amount"]}, None

def migrate(rows) -> MigrationResult:    result = MigrationResult()    for row in rows:        converted, reason = convert(row)        if converted is None:            result.rejected.append({"row": row, "reason": reason})        else:            result.converted.append(converted)    return result

変換できなかった行を `rejected` に貯めるのが要点です。件数と理由が分かれば、「この理由の行が3件だけなら手作業で直す」「1万件あるなら規則が足りない」という判断ができます。

黙って捨てると後で発覚する

変換できない行を `try` で握りつぶす実装をよく見ます。移行は成功したように見えて、数か月後に「あの取引が無い」と言われる。件数が合わないことに気づけるよう、必ず記録して突き合わせてください。

4. 一度で終わらせない

移行を切り替え当日に一括で行う計画は、たいてい破綻します。時間が足りず、失敗しても戻す余裕がありません。

現実的なのは、まず全件を移し、その後は差分を繰り返し反映する形です。切り替え直前に流すのは、最後の短い期間の差分だけになります。

  1. 1全件移行を1回行う — 時間がかかってよい。業務時間外でなくてもよい
  2. 2差分反映を繰り返す — 日次などで、新しく増えた分・変わった分を反映
  3. 3 を回す — 件数と代表値が一致しているか毎回確認
  4. 4切り替え時は最後の差分だけ — 短時間で終わる

2番目を繰り返すことで、変換規則の不備が早い段階で見つかります。1回きりの移行では、当日に初めて問題が発覚します。何度も練習できるというのが、この方式の最大の利点です。加えて、繰り返すうちに所要時間が読めるようになります。当日どれくらいかかるかを、推測ではなく実績で言えるのは、計画を立てるうえで大きい。

5. 一致していることを確認する

移した後、本当に同じものが入っているかを確認します。目視では無理なので、機械的に突き合わせます。

新旧の突き合わせ
SQL
-- 1. 件数(最も基本。ここがずれたら他を見る前に原因を探す)SELECT    (SELECT COUNT(*) FROM legacy_orders WHERE created_at >= '2024-01-01') AS,    (SELECT COUNT(*) FROM new_orders    WHERE created_at >= '2024-01-01') AS;
-- 2. 集計値(金額の合計は最も気づきやすい指標)SELECT    DATE_TRUNC('month', created_at) AS,    SUM(amount)                     AS 合計金額,    COUNT(*)                        AS 件数FROM legacy_ordersWHERE created_at >= '2024-01-01'GROUP BY 1 ORDER BY 1;-- 同じクエリを新側でも実行し、月ごとに突き合わせる
-- 3. 個別の行(サンプリングして中身まで確認)SELECT o.id, o.status AS 旧状態, n.status AS 新状態,       o.amount AS 旧金額, n.amount AS 新金額FROM legacy_orders oJOIN new_orders n ON n.legacy_id = o.idWHERE o.status IS DISTINCT FROM n.status   OR o.amount IS DISTINCT FROM n.amountLIMIT 100;

件数 → 集計値 → 個別の順に見るのが効率的です。件数が合わなければ、集計値を見る意味がありません。集計値が合っていれば、大きな取りこぼしは無いと判断できます。この突き合わせを まで毎回自動で回しておくと、差が出た日にすぐ気づけます。手作業で確認する方式にすると、忙しい時期に省略されて意味を失います。

6. どちらが正なのかを決めておく

移行期間中、新旧の両方にデータが存在します。ここで ── どちらが正しい値を持っているか ── を明確にしておかないと、ずれたときに勝者を決められません

段階ごとに正を1つに保つ
段階正となるのはもう片方の扱い
移行準備中新は検証用の複製
並行運用中旧のまま新は影として同じ内容を持つ
切り替え後旧は参照専用(書き込まない)
退役まで旧は停止準備

どの段階でも、正は常に片方だけです。「どちらも更新できる」状態を作ると、突き合わせで差が出たときにどちらが正しいか誰も答えられなくなります。EP.05 でも同じ指摘をしましたが、データ移行では特に厳密に守る必要があります。

ここまでのまとめ

どこまで移すかを先に決める(全部移すと古い歪みを持ち込む)。設計書ではなく実データを年ごとに集計して断層を探す。変換できない行は黙って捨てず記録する。移行は全件1回+差分の繰り返しにして、何度も練習する。突き合わせは件数→集計値→個別の順。そして正となるのは常に片方だけに保ってください。

シェア

この記事の感想を教えてください

あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp