「SQLiteを安全にバックアップする設計|Online Backup API・整合性検査・復元テスト」の内容を表す技術イラスト
UNIT 00LAB NOTES FIG.00 · EYECATCH

SQLiteを安全にバックアップする設計|Online Backup API・整合性検査・復元テスト

SQLiteの稼働中データベースをOnline Backup APIで整合snapshot化する設計を解説。一時file、整合性・外部キー検査、schema version、世代管理、Python実装、復元testまで整理します。

DOC NO.EKP-00-1154
DATE
REVB
READ約 12 分
目次

バックアップは「ファイルがある」だけでは成立しない

SQLiteは1ファイルで扱えるため、稼働中の.dbをそのままcopyすればバックアップになるように見えます。しかし、copy中に書き込みtransactionが進むと、copy先に古いpageと新しいpageが混在するおそれがあります。WAL modeでは、確定済みデータが-walに残っている場合もあります。

安全なバックアップとは、ある時点で整合し、実際に復元できるsnapshotです。必要な条件は次のとおりです。

  • 元databaseと別の保存先へ退避されている
  • 物理構造、参照関係、schema versionを検査できる
  • 必須tableや代表queryを確認できる
  • 再実行しても正常な世代を上書きしない
  • 隔離環境で復元testを実施している

この記事ではSQLite Online Backup APIを使い、Pythonで「取得→検証→確定→復元確認」までを一つの契約として設計します。

生file copyが危険になる理由

SQLiteはdatabase本体のほか、transaction処理中にrollback journalやWALを使います。稼働中の本体だけをOSのcopy commandで複製すると、copy開始時と終了時でpageの内容が変わる可能性があります。障害後に-journalや-walを本体から切り離すと、SQLiteが自動回復に必要な情報を失うこともあります。

Online Backup APIはSQLite自身がconnectionを通してpageを読み、destination databaseへcopyします。処理を分割できるため、sourceを長時間lockし続けず、稼働中でも整合したsnapshotを作れます。Python標準libraryのsqlite3.Connection.backup()から利用できます。

方式を目的で選ぶ

方式出力主な用途注意点
Online Backup APISQLite database稼働中DBの定期snapshotlock競合、容量、完了後検証が必要
VACUUM INTO最適化済みSQLite databasecompactなcopyCPU負荷が増え、incremental実行できない
.dump / iterdump()SQL text内容確認、移行補助復元時間と拡張機能の再現を確認する
停止後のfile copyDBと必要なjournal全接続を停止できる保守作業稼働中copyへ流用しない

VACUUM INTOは論理内容が同じcompactなdatabaseを作り、削除済み領域も除去します。一方、Backup APIはCPU消費が比較的小さく、page単位で進められます。「稼働中snapshot」「最小size」「SQLによる可搬性」のどれが必要かを先に決めます。

入力と出力の契約

backup jobの入力には、source識別子、実行を一意にするbackup_id、期待するschema version、必須table、destination、保管期限を含めます。

出力はSQLite fileとmanifestに分けます。manifestにはbackup_id、取得日時、byte数、SHA-256、schema version、検査結果を記録し、tokenやpassword、個人情報は複製しません。

{
  "backup_id": "parts-db-20261022T043100Z",
  "created_at": "2026-10-22T04:31:00Z",
  "database": "parts.db",
  "schema_version": 7,
  "size_bytes": 184320,
  "sha256": "<64 lowercase hexadecimal characters>",
  "integrity_check": "ok",
  "foreign_key_violations": 0
}

hashは転送後の破損検出には使えますが、正しいsourceを取得した証明にはなりません。schema version、必須table、業務件数、復元testを別に確認します。

一時fileで作り、検証後に確定する

最終fileへ直接backupすると、disk fullやprocess停止の際、不完全なfileを成功品と誤認するおそれがあります。次の順序にします。

  • final destinationと同じdirectoryへ一時fileを作る
  • Online Backup APIで一時fileへcopyする
  • connectionを閉じ、一時fileをread-onlyで検査する
  • 合格後にfinal nameへrenameする
  • hashとmanifestを作り、backup catalogへ成功を記録する

同じfilesystem内のrenameなら原子的に扱いやすくなります。final名にはUTC時刻やrun IDを含め、既存世代を上書きしません。期限切れ世代の削除はbackup作成とは別のjobにします。

PythonでOnline Backup APIを実装する

次の例はsourceをread-onlyで開き、一時fileへbackupし、検証後にfinal nameへ切り替えます。pagesは1回にcopyするpage数、sleepは残りを再試行するときの待機時間です。最適値はdatabase sizeと書き込み負荷を測って決めます。

from __future__ import annotations

import os
import sqlite3
import tempfile
from pathlib import Path


def validate_backup(
    path: Path,
    required_tables: set[str],
    expected_user_version: int,
) -> None:
    uri = f"{path.resolve().as_uri()}?mode=ro"
    with sqlite3.connect(uri, uri=True) as conn:
        result = conn.execute("PRAGMA integrity_check").fetchall()
        if result != [("ok",)]:
            raise RuntimeError(f"integrity_check failed: {result}")

        violations = conn.execute("PRAGMA foreign_key_check").fetchall()
        if violations:
            raise RuntimeError(f"foreign key violations: {violations}")

        version = conn.execute("PRAGMA user_version").fetchone()[0]
        if version != expected_user_version:
            raise RuntimeError(f"unexpected schema version: {version}")

        tables = {
            row[0]
            for row in conn.execute(
                """
                SELECT name FROM sqlite_schema
                WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
                """
            )
        }
        missing = required_tables - tables
        if missing:
            raise RuntimeError(f"missing tables: {sorted(missing)}")


def create_verified_backup(
    source: Path,
    destination: Path,
    required_tables: set[str],
    expected_user_version: int,
) -> None:
    source = source.resolve()
    destination = destination.resolve()
    destination.parent.mkdir(parents=True, exist_ok=True)

    if source == destination:
        raise ValueError("source and destination must differ")
    if destination.exists():
        raise FileExistsError(destination)

    handle, name = tempfile.mkstemp(
        prefix=f".{destination.name}.",
        suffix=".partial",
        dir=destination.parent,
    )
    os.close(handle)
    temp_path = Path(name)

    try:
        source_uri = f"{source.as_uri()}?mode=ro"
        with sqlite3.connect(
            source_uri, uri=True, timeout=10.0
        ) as src, sqlite3.connect(temp_path, timeout=10.0) as dst:
            src.backup(dst, pages=256, sleep=0.1)

        validate_backup(
            temp_path,
            required_tables,
            expected_user_version,
        )
        os.chmod(temp_path, 0o600)
        temp_path.rename(destination)
    except Exception:
        temp_path.unlink(missing_ok=True)
        raise

PRAGMA user_versionはapplicationがschema versionの整数を保存できる領域です。migrationを自動管理する機能ではないため、番号とmigration履歴はapplication側で管理します。

同名jobが複数processで走る構成では、backup catalogの一意制約や排他制御も必要です。object storageへ送る場合は、localで完成品を作ってからuploadし、remote側のversion IDとhashを記録します。

正常系と異常系を検証する

物理整合性と参照整合性

PRAGMA integrity_checkはpage、record、index、UNIQUE、CHECK、NOT NULLなどを検査し、問題がなければokを返します。ただし外部キー違反は対象外なので、PRAGMA foreign_key_checkを別に実行します。

大規模databaseで毎回の完全検査が重い場合、日次はquick_check、定期的にintegrity_checkという段階化もできます。ただしquick_checkはindex内容とtable内容の一致やUNIQUE制約など一部を省略します。検出範囲を運用手順へ明記します。

論理整合性

物理的に正常でも、空のdatabaseや別環境のdatabaseを取得した可能性があります。必須table、schema version、重要record数、最新更新時刻を確認します。稼働中sourceの現在件数との完全一致ではなく、完成snapshot内部で成立すべき条件を検証します。

復元test

backup成功の最終判定は、別pathへ復元して必要なqueryを実行できることです。本番へ上書きせず、隔離環境で次を確認します。

  • read-onlyで開ける
  • schema versionとapplication versionが対応する
  • 代表queryが期待する列と型を返す
  • 主要recordをIDで取得できる
  • 外部キーと業務上の集計が成立する
  • 復元所要時間が許容停止時間内に収まる

冪等性・世代管理・security

backup_idをcatalogの一意keyにし、状態をrunning、verified、failedへ分けます。verified済みのIDなら結果を再利用し、failedなら新しい一時fileからやり直します。失敗時はpartial fileをfinal名で残さず、成功catalogも書きません。

保存先容量不足、permission error、database lock、process停止を異常系testに含めます。retentionは日次・週次・月次などの回復点を残し、sourceと同じdiskだけへ保存しません。異なる権限、別storage、immutableなversionを組み合わせ、backup jobのcredentialは最小権限にします。

API・CAD・設計データへ再利用する

SQLiteをCAD属性、BOM、検査記録、API cacheに使う場合、manifestへapplication version、schema version、CAD revision、単位系、座標系、外部ID対応表のversionを残します。

SQLite外のCAD fileや画像はBackup APIではcopyされません。databaseが参照する外部assetを同じ回復点へそろえるには、version付きobjectやcontent hashで整合点を作ります。database backupだけでsystem全体を復元できると誤解しないことが重要です。

よくある失敗

  • 稼働中の.dbだけをcopyする:Backup APIかVACUUM INTOを使い、journalを手作業で分離しない。
  • 作成時点で成功にする:一時fileへ作り、検査と復元test後に確定する。
  • integrity_checkだけで十分と考える:foreign_key_checkと業務条件を追加する。
  • 同じfile名を上書きする:一意なbackup_idと世代管理を使う。
  • sourceと同じdiskだけへ保存する:別storage、別権限、retentionを設計する。
  • database外のassetを忘れる:CAD fileや添付fileもversionとhashで結ぶ。

まとめ

SQLiteのバックアップはfile複製ではなく、復元可能なデータ契約です。稼働中databaseにはOnline Backup APIを使い、一時fileへ整合snapshotを作ります。その後、integrity_check、foreign_key_check、schema version、必須table、代表queryを確認し、合格したfileだけを正式な世代として保存します。

backup ID、hash、manifest、retention、別storage、復元演習を組み合わせれば、「いつの何を、どのversionで、何分で戻せるか」を答えられる状態になります。

参考情報

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次