sqldiff.exeだけじゃない!SQLiteデータベース比較の代替手法を徹底解説

2025-05-27

sqldiff.exeとは

sqldiff.exe は、SQLiteデータベース間の差分を表示するためのコマンドラインユーティリティです。2つのSQLiteデータベースファイルの内容を比較し、一方のデータベースをもう一方のデータベースに変換するために必要なSQLスクリプトとして差分を出力します。

例えば、database1.sqlitedatabase2.sqlite と同じ状態にするにはどのような変更が必要かを、INSERTUPDATEDELETE ステートメントの形で示してくれます。

どのように動作するか

sqldiff.exe は、以下のロジックでデータベースの差分を検出します。

    • デフォルトでは、同じテーブル名を持ち、かつ同じ rowid を持つ行を「ペア」とみなします。
    • WITHOUT ROWID テーブルの場合は、同じ PRIMARY KEY を持つ行がペアとみなされます。
    • --primarykey オプションを使用すると、rowid があるテーブルでも、スキーマで定義された PRIMARY KEY を常にペアリングに使用するようになります。これは多くの場合、より正確な差分検出に適していますが、PRIMARY KEYNULL 値を持つ行がある場合は、差分を見落とす可能性があります。
  1. 差分の出力

    • ペアになった行の内容に違いがある場合
      UPDATE ステートメントとして出力されます。
    • ソースデータベース (database1.sqlite) にのみ存在する行(ペアが見つからない行)
      DELETE ステートメントとして出力されます。
    • 宛先データベース (database2.sqlite) にのみ存在する行(ペアが見つからない行)
      INSERT ステートメントとして出力されます。

主な機能とオプション

sqldiff.exe には、差分表示を制御するためのいくつかの便利なオプションがあります。

  • --vtab: 仮想テーブル(FTS3, FTS5, rtreeなど)の差分を直接含めるようにします。通常、仮想テーブルの実装がデータを格納するためにシャドウテーブル(実テーブル)を作成する場合、sqldiff.exe はこれらのシャドウテーブルの差分を計算しますが、このオプションを使用すると仮想テーブル自体の差分を扱います。
  • --transaction: 生成されるSQL出力を単一の大きなトランザクションで囲みます。
  • --table TABLE: データベース全体ではなく、指定したテーブル TABLE の内容の差分のみを表示します。
  • --summary: 各テーブルで変更された行数を表示しますが、実際の変更内容は表示しません。
  • --schema: テーブルの内容ではなく、スキーマ(テーブル定義、インデックスなど)の差分のみを表示します。
  • --primarykey: 上記で説明したように、rowid の代わりにスキーマ定義の PRIMARY KEY を使用して行をペアリングします。
  • --lib LIBRARY または -L LIBRARY: 差分計算前に、共有ライブラリやDLLファイルをSQLiteにロードします。これにより、スキーマで必要とされるアプリケーション定義の照合シーケンスなどを追加できます。
  • --changeset FILE: 標準出力にSQLスクリプトを出力する代わりに、変更セット(バイナリ形式)をファイルに書き込みます。この変更セットは、SQLiteのsessions拡張機能を使って解釈できます。
  • sqldiff [options] database1.sqlite database2.sqlite: 基本的な使用法です。database1.sqlite をソース、database2.sqlite を宛先として比較します。

sqldiff.exe にはいくつかの制限があります。

  • 仮想テーブルのデフォルトの動作
    デフォルトでは、仮想テーブルのスキーマや内容の差分は報告されません。ただし、仮想テーブルの実装がデータを格納するために実テーブル(シャドウテーブル)を作成する場合、sqldiff.exe はこれらのシャドウテーブル間の差分を計算します。これにより、生成されたSQLスクリプトを元のデータベースと完全に同じではないデータベースで実行した場合、予期せぬ結果(仮想テーブルの内容の破損など)が生じる可能性があります。
  • トリガーやビューの差分
    現在、トリガー(TRIGGER)やビュー(VIEW)の差分は表示されません。
  • アクセスできない rowid のテーブル
    rowid がアクセスできないテーブル(例: CREATE TABLE inaccessible_rowid("rowid" TEXT, "oid" TEXT, "_rowid_" TEXT); のようなテーブル)の差分を計算することはできません。


"sqldiff.exe" が見つからない / コマンドとして認識されない

エラーの症状
コマンドプロンプトやターミナルで sqldiff と入力しても、「'sqldiff' は、内部コマンドまたは外部コマンド、操作可能なプログラムまたはバッチ ファイルとして認識されていません。」といったエラーが表示される。

原因
sqldiff.exe がシステム環境変数 PATH に含まれていないか、SQLiteツールが正しくダウンロード・展開されていないためです。

トラブルシューティング

  1. SQLiteツールのダウンロードと展開
    公式SQLiteウェブサイト(sqlite.org)から「Precompiled Binaries for Windows」のsqlite-tools-win32-x86-*.zip (または64bit版) をダウンロードし、任意のフォルダ(例: C:\sqlite)に展開します。
  2. PATH 環境変数の設定
    • Windowsの場合:
      1. 「スタート」メニューを右クリックし、「システム」を選択します。
      2. 「システムの詳細設定」をクリックし、「環境変数」をクリックします。
      3. 「システム環境変数」セクションで Path を選択し、「編集」をクリックします。
      4. 「新規」をクリックし、sqldiff.exe が存在するフォルダのパス(例: C:\sqlite)を追加します。
      5. すべてのダイアログを「OK」で閉じ、コマンドプロンプトやターミナルを再起動して試します。
    • macOS/Linuxの場合: ~/.bashrc~/.zshrc などに export PATH="/path/to/sqlite/tools:$PATH" のように追記し、シェルを再起動します。

ファイルが見つからない / データベースが開けない

エラーの症状
sqldiff database1.sqlite database2.sqlite のように実行すると、「Error: unable to open database "database1.sqlite"」といったエラーが表示される。

原因
指定されたデータベースファイルが存在しない、またはパスが間違っている可能性があります。また、ファイルに対する読み取り権限がない場合もあります。

トラブルシューティング

  1. ファイルパスの確認
    コマンドで指定したファイルパスが正しいことを確認します。フルパスで指定してみるのが確実です。 例: sqldiff C:\Users\YourUser\Documents\database1.sqlite C:\Users\YourUser\Documents\database2.sqlite
  2. ファイル存在の確認
    該当するデータベースファイルが実際にその場所に存在するか確認します。
  3. ファイル権限の確認
    実行しているユーザーアカウントに、データベースファイルへの読み取り権限があることを確認します。

予期せぬ差分(DELETE/INSERTの多発)

エラーの症状
データベースの内容はほとんど変わっていないはずなのに、大量の DELETEINSERT ステートメントが出力される。UPDATE がほとんどない。

原因
sqldiff.exe が行を正しく「ペアリング」できていない場合に発生します。デフォルトでは、同じテーブル名と rowid を持つ行がペアとみなされます。WITHOUT ROWID テーブルの場合は PRIMARY KEY が使用されます。

トラブルシューティング

    • 多くのテーブルでは rowid が隠れて存在しますが、データの内容に基づいて行を比較したい場合は、PRIMARY KEY を使って比較する方が正確です。
    • sqldiff --primarykey database1.sqlite database2.sqlite のように実行してみてください。これにより、PRIMARY KEY が同じ行がペアとみなされ、UPDATE ステートメントが適切に生成される可能性が高まります。
    • 注意点
      PRIMARY KEYNULL 値を含む行がある場合、このオプションを使用すると、その行の差分が見落とされる可能性があります。
  1. テーブルスキーマの確認

    • 両方のデータベースで、比較したいテーブルのスキーマ(特に PRIMARY KEY やユニーク制約)が一致しているか確認します。スキーマが異なる場合、sqldiff は行を正しくペアリングできないことがあります。
    • sqldiff --schema database1.sqlite database2.sqlite でスキーマの差分を確認できます。

特定のテーブルの差分が表示されない / 正しくない

エラーの症状
期待するテーブルの差分が全く表示されない、または内容が間違っているように見える。

原因

  • 明示的なPRIMARY KEYがないテーブル
    WITHOUT ROWID テーブルで明示的な PRIMARY KEY が定義されていない場合、sqldiff は差分を計算できません。
  • rowid がアクセスできないテーブル
    特定の条件で rowid がアクセスできないテーブル(例: CREATE TABLE inaccessible("rowid" TEXT); のように rowid という名前の列を定義している場合)は、差分を計算できません。
  • 仮想テーブル (Virtual Tables)
    FTS3/FTS5、rtreeなどの仮想テーブルの差分は、デフォルトでは報告されません。

トラブルシューティング

  1. --vtab オプションの使用 (仮想テーブルの場合)
    • 仮想テーブルの差分を直接含めるには、--vtab オプションを使用します。
    • sqldiff --vtab database1.sqlite database2.sqlite
    • これにより、FTS3/FTS5やrtreeなどの仮想テーブルの下層にあるシャドウテーブルの差分ではなく、仮想テーブル自体の差分が扱われます。
  2. テーブルスキーマの確認
    • PRAGMA table_info(テーブル名); を使用して、問題のテーブルのスキーマを確認します。rowid のアクセス性や PRIMARY KEY の定義を注意深く調べます。
    • sqldiff の制限事項に合致するテーブルではないか確認してください。

トリガーやビューの差分が表示されない

エラーの症状
トリガーやビューの定義が変更されているにもかかわらず、sqldiff の出力にそれらの差分が含まれない。

原因
sqldiff.exe は、現在のところトリガー (TRIGGER) やビュー (VIEW) の差分を表示する機能を持っていません。これは既知の制限事項です。

トラブルシューティング

  • 手動での比較
    トリガーやビューの差分を確認するには、sqlite_master テーブルから定義を取得し、テキストエディタや一般的な差分ツール(diffコマンドなど)で比較するしかありません。
    -- データベース1からトリガー/ビューの定義を取得
    .output db1_triggers_views.sql
    SELECT sql FROM sqlite_master WHERE type IN ('trigger', 'view');
    .quit
    
    -- データベース2からトリガー/ビューの定義を取得
    .output db2_triggers_views.sql
    SELECT sql FROM sqlite_master WHERE type IN ('trigger', 'view');
    .quit
    
    その後、diff db1_triggers_views.sql db2_triggers_views.sql のように比較します。

出力されるSQLが構文エラーを起こす

エラーの症状
sqldiff が生成したSQLスクリプトを実行すると、SQL statement error: near "...": syntax error などの構文エラーが発生する。

原因
sqldiff 自体のバグ、または特定の複雑なスキーマやデータ型との組み合わせで予期せぬSQLが生成される可能性があります。

トラブルシューティング

  1. SQLiteのバージョンアップ
    sqldiff.exe はSQLiteツールの一部であるため、最新のSQLiteバージョンにアップデートすることで、既知のバグが修正されている可能性があります。
  2. 生成されたSQLの確認と修正
    • 生成されたSQLスクリプトを注意深く確認し、手動で構文エラーを修正します。
    • 特に、特殊な文字を含むデータや、NULL 値の扱いが問題となる場合があります。
  3. 問題の切り分け
    どのテーブルで問題が発生しているかを特定するために、--table TABLE_NAME オプションを使って個別のテーブルを比較してみます。

データベースがロックされている / ビジー状態

エラーの症状
sqldiff の実行中に、「database is locked」や「database is busy」といったエラーが表示される。

原因
比較しようとしているSQLiteデータベースファイルが、他のプロセス(別のアプリケーション、別のSQLite接続など)によって現在使用中であるため、排他的なアクセスができない状態になっています。

トラブルシューティング

  1. 他のアプリケーションの終了
    データベースファイルを使用している可能性のあるすべてのアプリケーションやプロセスを終了します。
  2. 排他ロックの解除
    データベースファイルがネットワーク共有上にある場合や、永続的なロックが発生している場合は、システムを再起動する必要があるかもしれません。
  3. データベースのコピー
    一時的な解決策として、比較したいデータベースファイルのコピーを作成し、そのコピーに対して sqldiff を実行します。これにより、元のデータベースの使用に影響を与えずに差分を確認できます。


SQLiteの sqldiff.exe は、通常、コマンドラインから直接実行するユーティリティであり、特定のプログラミング言語から「直接」sqldiff.exe のコードを呼び出して処理を行うというよりは、プログラムの中からsqldiff.exe コマンドを実行し、その出力を受け取って解析するという形で連携します。

ここでは、Pythonを例にとり、プログラム内で sqldiff.exe を利用する基本的な方法と、その出力を処理する例をいくつかご紹介します。

Pythonでは、subprocess モジュールを使って外部コマンドを実行できます。

データベースの作成と初期データの投入

まず、比較対象となる2つのSQLiteデータベースファイルを作成しましょう。

import sqlite3
import os

def create_and_populate_db(db_name, initial_data):
    """データベースを作成し、データを投入するヘルパー関数"""
    if os.path.exists(db_name):
        os.remove(db_name) # 既存のファイルを削除
    conn = sqlite3.connect(db_name)
    cursor = conn.cursor()
    
    cursor.execute("""
        CREATE TABLE users (
            id INTEGER PRIMARY KEY,
            name TEXT NOT NULL,
            age INTEGER
        );
    """)
    
    cursor.executemany("INSERT INTO users (id, name, age) VALUES (?, ?, ?);", initial_data)
    
    conn.commit()
    conn.close()
    print(f"データベース '{db_name}' を作成しました。")

# データベース1のデータ
data1 = [
    (1, "Alice", 30),
    (2, "Bob", 25),
    (3, "Charlie", 35)
]
create_and_populate_db("db1.sqlite", data1)

# データベース2のデータ(db1からの変更を含む)
data2 = [
    (1, "Alice", 31),    # Aliceのageが変更
    (2, "Robert", 25),   # Bobの名前がRobertに変更
    (4, "David", 40)    # 新規追加
    # Charlieはdb2に存在しないため、DELETEされる
]
create_and_populate_db("db2.sqlite", data2)

print("--- データベース準備完了 ---")

基本的な差分比較

最も基本的な sqldiff の使い方です。subprocess.run() を使ってコマンドを実行し、標準出力に差分を表示します。

import subprocess

db1_path = "db1.sqlite"
db2_path = "db2.sqlite"

try:
    # sqldiff コマンドを実行
    # capture_output=True で標準出力をキャプチャ
    # text=True で出力をテキストとしてデコード
    result = subprocess.run(
        ["sqldiff", db1_path, db2_path],
        capture_output=True,
        text=True,
        check=True # エラーが発生した場合にCalledProcessErrorを発生させる
    )
    
    print("\n--- sqldiff 基本的な差分出力 ---")
    print(result.stdout)

except FileNotFoundError:
    print("エラー: 'sqldiff.exe' が見つかりません。PATHが正しく設定されているか確認してください。")
except subprocess.CalledProcessError as e:
    print(f"エラー: sqldiff の実行中に問題が発生しました。")
    print(f"標準エラー出力:\n{e.stderr}")

出力例

--- sqldiff 基本的な差分出力 ---
DELETE FROM users WHERE id=3 AND name='Charlie' AND age=35;
UPDATE users SET name='Robert' WHERE id=2;
UPDATE users SET age=31 WHERE id=1;
INSERT INTO users(id,name,age) VALUES(4,'David',40);

--primarykey オプションを使った差分比較

行を rowid ではなく PRIMARY KEY でペアリングしたい場合に --primarykey オプションを使用します。これにより、予期せぬ DELETE/INSERT が減り、より直感的な UPDATE が生成されることがあります。

import subprocess

db1_path = "db1.sqlite"
db2_path = "db2.sqlite"

try:
    print("\n--- sqldiff --primarykey オプションを使った差分出力 ---")
    result = subprocess.run(
        ["sqldiff", "--primarykey", db1_path, db2_path],
        capture_output=True,
        text=True,
        check=True
    )
    print(result.stdout)

except FileNotFoundError:
    print("エラー: 'sqldiff.exe' が見つかりません。PATHが正しく設定されているか確認してください。")
except subprocess.CalledProcessError as e:
    print(f"エラー: sqldiff の実行中に問題が発生しました。")
    print(f"標準エラー出力:\n{e.stderr}")

この例では、users テーブルにid列がPRIMARY KEYとして定義されているため、先の例と同じ出力になる可能性が高いですが、rowidが何らかの理由で変化した場合などに違いが出ます。

スキーマ差分の取得 (--schema)

テーブルの内容ではなく、データベースのスキーマ(テーブル定義、インデックスなど)の差分だけを取得したい場合に --schema オプションを使用します。

import subprocess
import sqlite3
import os

# スキーマ変更用のDBをもう一つ作成
def create_db_with_schema_change(db_name):
    if os.path.exists(db_name):
        os.remove(db_name)
    conn = sqlite3.connect(db_name)
    cursor = conn.cursor()
    
    cursor.execute("""
        CREATE TABLE products (
            product_id INTEGER PRIMARY KEY,
            product_name TEXT NOT NULL
        );
    """)
    conn.commit()
    conn.close()

# 既存のdb1.sqliteに新しいテーブルを追加したdb3.sqliteを作成
db3_path = "db3.sqlite"
if os.path.exists(db3_path):
    os.remove(db3_path)
import shutil
shutil.copy("db1.sqlite", db3_path) # db1をコピー

conn_db3 = sqlite3.connect(db3_path)
cursor_db3 = conn_db3.cursor()
cursor_db3.execute("""
    CREATE TABLE orders (
        order_id INTEGER PRIMARY KEY,
        user_id INTEGER,
        order_date TEXT
    );
""")
conn_db3.commit()
conn_db3.close()
print(f"データベース '{db3_path}' に新しいテーブル 'orders' を追加しました。")

try:
    print("\n--- sqldiff --schema オプションを使ったスキーマ差分出力 ---")
    result = subprocess.run(
        ["sqldiff", "--schema", db1_path, db3_path],
        capture_output=True,
        text=True,
        check=True
    )
    print(result.stdout)

except FileNotFoundError:
    print("エラー: 'sqldiff.exe' が見つかりません。PATHが正しく設定されているか確認してください。")
except subprocess.CalledProcessError as e:
    print(f"エラー: sqldiff の実行中に問題が発生しました。")
    print(f"標準エラー出力:\n{e.stderr}")

出力例

--- sqldiff --schema オプションを使ったスキーマ差分出力 ---
CREATE TABLE orders(order_id INTEGER PRIMARY KEY,user_id INTEGER,order_date TEXT);

差分結果をパースして利用する

sqldiff の出力はSQLステートメントなので、これらを文字列として受け取り、プログラム内でさらに処理することができます。例えば、差分の種類(INSERT, UPDATE, DELETE)ごとにカウントしたり、特定の条件に合致する差分だけを抽出したりできます。

import subprocess

db1_path = "db1.sqlite"
db2_path = "db2.sqlite"

try:
    result = subprocess.run(
        ["sqldiff", db1_path, db2_path],
        capture_output=True,
        text=True,
        check=True
    )
    
    diff_output = result.stdout
    
    # 差分を解析
    diff_lines = diff_output.strip().split('\n')
    
    insert_count = 0
    update_count = 0
    delete_count = 0
    
    parsed_diffs = []

    for line in diff_lines:
        line = line.strip()
        if line.startswith("INSERT"):
            insert_count += 1
            parsed_diffs.append({"type": "INSERT", "sql": line})
        elif line.startswith("UPDATE"):
            update_count += 1
            parsed_diffs.append({"type": "UPDATE", "sql": line})
        elif line.startswith("DELETE"):
            delete_count += 1
            parsed_diffs.append({"type": "DELETE", "sql": line})
        else:
            # その他の行(コメントなど)
            pass

    print("\n--- 解析された差分情報 ---")
    print(f"INSERT ステートメント数: {insert_count}")
    print(f"UPDATE ステートメント数: {update_count}")
    print(f"DELETE ステートメント数: {delete_count}")
    
    print("\n--- すべての差分ステートメント ---")
    for diff in parsed_diffs:
        print(f"タイプ: {diff['type']}, SQL: {diff['sql']}")

except FileNotFoundError:
    print("エラー: 'sqldiff.exe' が見つかりません。PATHが正しく設定されているか確認してください。")
except subprocess.CalledProcessError as e:
    print(f"エラー: sqldiff の実行中に問題が発生しました。")
    print(f"標準エラー出力:\n{e.stderr}")

finally:
    # 後処理:作成したデータベースファイルを削除
    for db_file in ["db1.sqlite", "db2.sqlite", "db3.sqlite"]:
        if os.path.exists(db_file):
            os.remove(db_file)
    print("\n--- クリーンアップ完了 ---")

出力例 (解析部分)

--- 解析された差分情報 ---
INSERT ステートメント数: 1
UPDATE ステートメント数: 2
DELETE ステートメント数: 1

--- すべての差分ステートメント ---
タイプ: DELETE, SQL: DELETE FROM users WHERE id=3 AND name='Charlie' AND age=35;
タイプ: UPDATE, SQL: UPDATE users SET name='Robert' WHERE id=2;
タイプ: UPDATE, SQL: UPDATE users SET age=31 WHERE id=1;
タイプ: INSERT, SQL: INSERT INTO users(id,name,age) VALUES(4,'David',40);

このように、sqldiff.exe はコマンドラインツールであるため、プログラミング言語からは subprocess モジュールなどを介して「外部コマンドとして実行し、その出力をキャプチャして利用する」というアプローチが一般的です。この方法を使えば、データベースの変更を自動的に検出し、それを基にした様々な処理(ログ記録、自動同期スクリプトの生成、通知など)をプログラムに組み込むことができます。



主な代替方法をいくつかご紹介します。

各データベースからデータを読み込み、プログラム内で比較する

これは最も直接的で、かつ最も柔軟な方法です。両方のデータベースからテーブルごとにデータを読み込み、プログラムのメモリ上で比較ロジックを実装します。

メリット

  • 比較中にデータを前処理したり、結合したりできる。
  • 差分の出力形式を自由に制御できる(SQL、JSON、カスタムフォーマットなど)。
  • 差分検出のロジックを完全にカスタマイズできる(例:特定の列だけを比較、複雑な比較ルール)。
  • sqldiff.exe のような外部ツールへの依存がないため、デプロイや環境設定が簡単。

デメリット

  • パフォーマンスを最適化するための考慮が必要。
  • 差分検出のロジック(特に UPDATEDELETE/INSERT の区別)を自分で実装する必要があるため、開発コストがかかる。
  • 大規模なデータベースの場合、メモリ使用量が多くなる可能性がある。

実装例 (Python)

import sqlite3

def get_table_data(db_path, table_name, primary_key_columns):
    """指定されたテーブルのデータを辞書のリストとして取得する"""
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()
    cursor.execute(f"PRAGMA table_info({table_name});")
    columns_info = cursor.fetchall()
    column_names = [col[1] for col in columns_info]

    cursor.execute(f"SELECT * FROM {table_name};")
    rows = cursor.fetchall()
    conn.close()

    # プライマリキーをキーとする辞書を作成
    data_dict = {}
    for row in rows:
        row_dict = dict(zip(column_names, row))
        pk_values = tuple(row_dict[col] for col in primary_key_columns)
        data_dict[pk_values] = row_dict
    return data_dict

def compare_tables(db1_path, db2_path, table_name, primary_key_columns):
    """2つのデータベースの指定されたテーブルを比較し、差分を生成する"""
    data1 = get_table_data(db1_path, table_name, primary_key_columns)
    data2 = get_table_data(db2_path, table_name, primary_key_columns)

    inserts = []
    updates = []
    deletes = []

    # DELETE と UPDATE を検出
    for pk, row1 in data1.items():
        if pk not in data2:
            # db2に存在しない行はDELETE
            deletes.append(row1)
        else:
            row2 = data2[pk]
            if row1 != row2: # 行の内容が異なる場合はUPDATE
                updated_cols = {}
                for col_name in row1.keys():
                    if row1[col_name] != row2[col_name]:
                        updated_cols[col_name] = row2[col_name]
                updates.append({"pk_values": pk, "old_row": row1, "new_values": updated_cols})

    # INSERT を検出
    for pk, row2 in data2.items():
        if pk not in data1:
            # db1に存在しない行はINSERT
            inserts.append(row2)
            
    return inserts, updates, deletes

# --- 実行例 ---
db1_path = "db1.sqlite" # 前の例で作成したデータベースを使用
db2_path = "db2.sqlite"

# users テーブルを比較 (id をプライマリキーとして)
inserts, updates, deletes = compare_tables(db1_path, db2_path, "users", ["id"])

print("--- カスタム比較結果 ---")
print("\nINSERTs:")
for item in inserts:
    print(f"  INSERT INTO users VALUES {tuple(item.values())};") # シンプルなSQL生成

print("\nUPDATEs:")
for item in updates:
    pk_condition = " AND ".join([f"{col}='{val}'" for col, val in zip(["id"], item["pk_values"])])
    set_clause = ", ".join([f"{col}='{val}'" for col, val in item["new_values"].items()])
    print(f"  UPDATE users SET {set_clause} WHERE {pk_condition};")

print("\nDELETEs:")
for item in deletes:
    pk_condition = " AND ".join([f"{col}='{val}'" for col, val in zip(["id"], item["pk_values"])])
    print(f"  DELETE FROM users WHERE {pk_condition};")

この例は非常に簡略化されており、実際のSQL生成はより複雑になりますが、基本的なアプローチを示しています。

SQLレベルでの比較 (複雑なクエリ)

これは、2つのデータベースをアタッチ (ATTACH DATABASE) し、SQLクエリを使って直接差分を抽出する方法です。

メリット

  • rowidPRIMARY KEY のペアリングロジックをSQLで直接表現できる。
  • SQLの知識があれば、差分クエリを記述できる。
  • 外部ツールへの依存がない。

デメリット

  • 特に UPDATE の検出はトリッキーになる。
  • ATTACH DATABASE は同じファイルシステム上でのみ機能し、ネットワーク上のファイルなどには制限がある場合がある。
  • 差分を正確に抽出するためのSQLクエリが非常に複雑になる可能性がある。

実装例 (擬似コード)

-- db2.sqlite を db1.sqlite にアタッチ
ATTACH DATABASE 'db2.sqlite' AS db2;

-- INSERT (db2 にのみ存在する行)
SELECT * FROM db2.users
EXCEPT
SELECT * FROM main.users;

-- DELETE (db1 にのみ存在する行)
SELECT * FROM main.users
EXCEPT
SELECT * FROM db2.users;

-- UPDATE の検出はより複雑
-- (例: id が同じだが他の列が異なる行)
SELECT T1.*, T2.*
FROM main.users AS T1
INNER JOIN db2.users AS T2 ON T1.id = T2.id
WHERE T1.name <> T2.name OR T1.age <> T2.age;

この方法で UPDATE のSQLを生成するには、さらに複雑なロジックが必要です。例えば、UPDATE された各列を特定し、それらを SET 句に含める必要があります。

ORM/データベースマイグレーションツールを利用する

多くのプログラミング言語には、ORM (Object-Relational Mapping) やデータベースマイグレーションツールがあります。これらのツールは、スキーマの変更を管理したり、データの差分を検出・適用する機能の一部を提供している場合があります。


  • Liquibase (Java/汎用)
    データベーススキーマとデータの変更をバージョン管理し、差分を適用できます。
  • Flyway (Java)
    SQLベースのマイグレーションツールで、スキーマの変更を管理します。
  • Ruby on Rails (Ruby)
    Active Record Migrations はスキーマ変更を扱います。
  • SQLAlchemy (Python)
    Alembic などのマイグレーションツールと組み合わせて使用できます。
  • Django (Python)
    makemigrationsmigrate コマンドがスキーマの変更を検出し、SQLを生成します。データの差分には直接対応していませんが、カスタムマイグレーションで処理できます。

メリット

  • 複雑な差分検出ロジックを自分で実装する必要がない場合がある。
  • スキーマ変更の管理が容易になる。
  • フレームワークやツールに統合されており、開発ワークフローに馴染む。

デメリット

  • 主にスキーママイグレーションに焦点を当てており、任意の一時的なデータ比較には向かない。
  • データそのものの詳細な差分比較は、追加のコードやプラグインが必要な場合がある。
  • 特定のフレームワークやツールにロックインされる可能性がある。

変更セット/レプリケーションツールを利用する

SQLiteには、データベースの変更を記録し、それを別のデータベースに適用するための「変更セット(changeset)」と「セッション(session)」という機能があります。これは sqldiff とは異なるアプローチで、連続的な変更の同期に適しています。

  • sqldiff --changeset オプションは、この変更セットを生成するために利用できます。
  • sqlite3_changeset_start() / sqlite3_changeset_next()
    データベースに加えられた変更をバイナリ形式の変更セットとして記録・再生するAPI。

メリット

  • 変更セットはコンパクトなバイナリ形式で、転送効率が良い。
  • レプリケーションや同期のシナリオに非常に適している。
  • きめ細やかな変更の記録と適用が可能。

デメリット

  • APIの学習コストがある。
  • sqldiff のように「2つの時点のデータベースの差分を計算する」のではなく、「ある時点から別の時点までの変更を記録する」というアプローチ。そのため、既存の2つのデータベースの純粋な差分を求めるには、sqldiff --changeset を使うか、変更セットAPIを独自に実装して差分を生成する必要がある。

どの方法を選ぶべきか?

  • SQLに慣れており、データベース内部で差分を処理したい場合
    ATTACH DATABASE と複雑なSQLクエリを組み合わせる方法も選択肢になりますが、UPDATE の検出は難しいです。
  • 継続的なスキーマ変更やデータ変更の管理、バージョン管理を重視する場合
    ORMやデータベースマイグレーションツール、あるいはSQLiteの変更セット/レプリケーション機能の利用を検討してください。
  • 特定の列のみを比較したい、カスタムな比較ロジックが必要、または外部ツールへの依存をなくしたい場合
    各データベースからデータを読み込み、プログラム内で比較する方法が適しています。
  • 簡単なワンショット比較や、sqldiff.exe の出力形式で十分な場合
    sqldiff.exesubprocess で呼び出すのが最も手軽で効率的です。

ご自身のプロジェクトの要件、パフォーマンス要件、開発コスト、および柔軟性のニーズに応じて、最適な方法を選択してください。 SQLiteの sqldiff.exe は非常に便利ですが、プログラミングにおいて直接 sqldiff.exe を外部プロセスとして呼び出す以外にも、SQLiteデータベースの差分を検出・処理するための代替手段がいくつか存在します。これらの方法は、より柔軟な制御、特定の言語環境への統合、またはパフォーマンスの最適化を目的とする場合があります。

プログラミング言語のSQLiteライブラリを使った手動での比較

これは最も柔軟な方法で、Pythonの sqlite3 モジュール、Javaの JDBCドライバ、C# の Microsoft.Data.Sqlite など、各言語のSQLiteライブラリを使ってデータベースに接続し、SQLクエリを発行してデータを取得し、プログラム内で比較ロジックを実装します。

メリット

  • メモリ内処理
    データをメモリにロードして比較することで、ディスクI/Oを減らせる場合があります(ただし、大規模データには注意が必要)。
  • プラットフォーム非依存性
    sqldiff.exe のような外部ツールへの依存がなくなります。
  • 言語への統合
    既存のアプリケーションロジックにシームレスに統合できます。
  • 完全な制御
    差分検出のロジックを完全にカスタマイズできます。特定の列だけを比較したり、独自のペアリング基準を定義したり、差分の表示形式を自由に調整したりできます。

デメリット

  • 網羅性
    sqldiff のような専用ツールが持つ、スキーマの比較や仮想テーブルの扱いといった高度な機能を自力で網羅するのは困難です。
  • パフォーマンス
    大規模なデータベースの場合、すべてのデータをメモリにロードしたり、複雑なSQLクエリを頻繁に実行したりすると、パフォーマンスの問題が発生する可能性があります。
  • 実装コスト
    差分検出ロジック(INSERT, UPDATE, DELETEの識別)を自分で実装する必要があり、手間がかかります。

Pythonでの例(手動での比較ロジックの一部):

import sqlite3

def get_table_data(db_path, table_name, primary_key_col):
    """テーブルのデータを辞書のリストとして取得する"""
    conn = sqlite3.connect(db_path)
    conn.row_factory = sqlite3.Row # 行を辞書のようにアクセスできるようにする
    cursor = conn.cursor()
    cursor.execute(f"SELECT * FROM {table_name} ORDER BY {primary_key_col};")
    data = {row[primary_key_col]: dict(row) for row in cursor.fetchall()}
    conn.close()
    return data

def compare_tables_manual(db1_path, db2_path, table_name, primary_key_col):
    db1_data = get_table_data(db1_path, table_name, primary_key_col)
    db2_data = get_table_data(db2_path, table_name, primary_key_col)

    inserts = []
    updates = []
    deletes = []

    # db1にあってdb2にない行をDELETEとして検出
    for pk, row_db1 in db1_data.items():
        if pk not in db2_data:
            deletes.append(row_db1)
        else:
            # 両方に存在する行の差分をUPDATEとして検出
            row_db2 = db2_data[pk]
            if row_db1 != row_db2: # 辞書として比較
                # 変更されたカラムを特定するより詳細なロジックをここに追加可能
                updates.append({"old": row_db1, "new": row_db2})

    # db2にあってdb1にない行をINSERTとして検出
    for pk, row_db2 in db2_data.items():
        if pk not in db1_data:
            inserts.append(row_db2)
            
    return inserts, updates, deletes

# 使用例(上記のデータ作成コードを前提)
# inserts, updates, deletes = compare_tables_manual("db1.sqlite", "db2.sqlite", "users", "id")

# print("\n--- 手動比較の結果 ---")
# print(f"INSERTs: {inserts}")
# print(f"UPDATEs: {updates}")
# print(f"DELETEs: {deletes}")

SQLの ATTACH DATABASE と EXCEPT/UNION を使った比較

SQLiteでは、ATTACH DATABASE コマンドを使って複数のデータベースファイルを同じ接続内で同時に扱うことができます。これにより、SQLクエリ自体を使ってデータベース間の差分を検出することが可能です。

メリット

  • 外部ツール不要
    sqldiff.exe をインストールする必要がありません。
  • データベースエンジンによる最適化
    SQLiteエンジンが効率的に差分を計算します。
  • SQLネイティブ
    SQLの知識があれば簡単に実装できます。

デメリット

  • パフォーマンス
    大規模なテーブルで EXCEPT を多用すると、パフォーマンスに影響が出る可能性があります。
  • UPDATE の検出が複雑
    INSERTDELETEEXCEPT で比較的容易ですが、UPDATE を検出するには、両方のテーブルからデータを取得してプログラム側で比較するか、より複雑な JOINWHERE 句を組み合わせる必要があります。
  • スキーマの差分は扱えない
    主にデータの差分(行の追加、削除、変更)に限定されます。スキーマの変更は検出できません。

Pythonでの例(SQLでの差分検出):

import sqlite3
import os

def compare_db_with_sql(db1_path, db2_path, table_name, primary_key_col):
    conn = sqlite3.connect(db1_path)
    
    # db2をアタッチ
    conn.execute(f"ATTACH DATABASE '{db2_path}' AS db2;")

    print(f"\n--- SQLによる差分検出 ({table_name}) ---")

    # db1にありdb2にない行 (DELETEされる行)
    # primary_key_col で比較し、rowid に依存しないようにする
    cursor = conn.execute(f"""
        SELECT * FROM {table_name}
        EXCEPT
        SELECT * FROM db2.{table_name};
    """)
    deletes = cursor.fetchall()
    print(f"DELETEされる行: {deletes}")

    # db2にありdb1にない行 (INSERTされる行)
    cursor = conn.execute(f"""
        SELECT * FROM db2.{table_name}
        EXCEPT
        SELECT * FROM {table_name};
    """)
    inserts = cursor.fetchall()
    print(f"INSERTされる行: {inserts}")

    # UPDATEされた行を検出するのはより複雑で、変更された列を特定するにはさらにSQLが必要
    # 例: primary keyが一致するが、他のカラムが異なる行
    cursor = conn.execute(f"""
        SELECT T1.*, T2.* FROM {table_name} AS T1
        INNER JOIN db2.{table_name} AS T2 ON T1.{primary_key_col} = T2.{primary_key_col}
        WHERE T1.name != T2.name OR T1.age != T2.age; -- 変更されたカラムの条件
    """)
    updates_raw = cursor.fetchall()
    print(f"UPDATEされた可能性のある行(完全な変更内容は別途解析が必要): {updates_raw}")
    
    conn.close()

# 使用例(上記のデータ作成コードを前提)
# compare_db_with_sql("db1.sqlite", "db2.sqlite", "users", "id")

ORM (Object-Relational Mapping) を使った比較

Django ORM (Python)、Entity Framework (.NET)、Hibernate (Java) など、ORMツールはデータベースの抽象化レイヤーを提供します。これらを使って両方のデータベースからデータをオブジェクトとして取得し、オブジェクトの比較ロジックを実装することで差分を検出できます。

メリット

  • データ構造の抽象化
    データベースのスキーマ変更に強いコードを書けます(ORMのマイグレーション機能と組み合わせる場合)。
  • 開発効率
    定型的なSQL記述を減らせます。
  • オブジェクト指向
    データベース操作をオブジェクトとして扱えるため、コードが読みやすくなります。

デメリット

  • 複雑なクエリの限界
    複雑な差分検出ロジックは、ORMの範疇を超えて生SQLを書く必要がある場合があります。
  • 学習コスト
    ORMの概念と使用方法を習得する必要があります。
  • パフォーマンスオーバーヘッド
    ORMには固有のオーバーヘッドがあり、大量のデータを扱う場合には効率が悪い可能性があります。

SQLiteに特化した、または一般的なデータベース比較ツールの中には、プログラミング言語から利用できるAPIや、コマンドラインで強力な差分検出機能を提供するものがあります。

  • サードパーティライブラリ
    • 一部の言語では、SQLiteデータベースの比較に特化した非公式のライブラリが存在する場合があります。GitHubなどで「sqlite diff [言語名]」で検索すると見つかる可能性があります。
    • 例えば、Pythonではsqlite-diff.pyのようなスクリプトがGitHubに公開されていることがあります。これらはsqldiff.exeと同様に内部でSQLクエリを発行したり、テキストファイルを比較したりするロジックをラップしていることが多いです。
  • GUIベースのツール
    • DB Browser for SQLite
      GUIツールですが、データベースの比較機能を持つ場合があります。
    • Redgate SQL Compare (SQL Server向けですが、同種のツールがSQLiteにも存在する可能性あり)
      高度な商用ツールは、スキーマとデータの両方を比較し、同期スクリプトを生成できます。

メリット

  • 高速性
    最適化されたアルゴリズムで差分検出を行うため、効率的です。
  • 使いやすさ
    GUIツールは直感的に操作できます。
  • 高機能
    sqldiff.exe 以上の詳細な比較オプションやレポート機能を持つ場合があります。
  • カスタマイズ性の限界
    提供されている機能以上のカスタマイズは難しい場合があります。
  • ライセンス
    商用ツールの場合、ライセンス費用がかかります。
  • 依存性の追加
    外部ツールやライブラリへの依存が発生します。
  • 強力な機能やGUIが欲しい場合、または特定のニーズに特化したツールが必要な場合
    専用のデータベース比較ツールやライブラリを検討します。
  • 大規模なアプリケーションでデータベース操作を抽象化したい場合
    ORMの利用を検討しますが、差分検出は追加のロジックが必要です。
  • SQLの知識があり、データ差分に重点を置く場合
    ATTACH DATABASEEXCEPT/UNION を使ったSQLベースの比較が効率的です。
  • プログラム内で完全に制御し、カスタムロジックを実装したい場合
    各言語のSQLiteライブラリを使った手動比較が適しています。特に、データ量が多くない場合や、特定の列の差分だけを気にする場合に有効です。
  • 簡単なスクリプトや迅速なチェックの場合
    sqldiff.exe を直接呼び出すのが最も手軽で迅速です。