1 ポイント 投稿者 GN⁺ 2025-05-02 | 1件のコメント | WhatsAppで共有
  • 大きなSQLiteデータベースをリモートサーバーからローカルへ移すとき、元の.dbの代わりに gzip 圧縮した SQL ダンプをコピーすると、転送量を大幅に減らせる
  • ファイルが大きくなる主な原因はインデックスであり、クエリを高速化するためにテーブルデータを重複保存してディスク使用量を増やす
  • SQLite の .dump はデータベースを SQL 文のテキストに変換し、インデックスは実データではなく CREATE INDEX 1行で表現されるため、重複転送を避けられる
  • 例のデータベースは元のサイズが3.4GB、ダンプテキストが1.3GB、gzip 圧縮版が240MBで、圧縮ダンプは元ファイルより14倍小さかった
  • コピー前にダンプファイルを固定しておけば、転送中に元データが変わって database disk image is malformed エラーになる状況を減らせる

SQLiteダンプを圧縮してコピーする

  • 小さな SQLite データベースなら rsync で直接コピーしても十分
    • 例: rsync --progress username@server:my_remote_database.db my_local_database.db
  • データベースが大きくなると、直接コピーは遅く不安定になることがある
    • 250MB のデータベースを家庭用インターネット経由でウェブサーバーからダウンロードするのに約1分かかる
    • 数 GB 規模のデータベースでは待ち時間がさらに大きくなる
  • サイズ増加のボトルネックはインデックスにある
    • インデックスはクエリ速度を大きく向上させるが、データベースファイルも肥大化させる
    • あるデータベースでは、特定のインデックス1つだけでディスクサイズの半分を占めていた
    • インデックスは固有のデータを持つというより、他のテーブルのデータを重複して保持して検索を高速化する
  • SQLite はデータベース全体をテキストファイルとしてダンプできる
    • コマンド: sqlite3 my_database.db .dump > my_database.db.txt
    • 出力されるファイルは CREATE TABLEINSERT INTOCREATE INDEX のような SQL 文で構成される
    • インデックスは実際のインデックスデータではなく、CREATE INDEX [idx_photo_locations] ON [photos] ([longitude], [latitude]); のような作成コマンドとして保存される
  • ダンプファイルは SQLite データベースに復元できる
    • コマンド: cat my_database.db.txt | sqlite3 my_reconstructed_database.db
  • SQL ダンプは繰り返しが多いため、gzip 圧縮の効率が高い
    • 例: sqlite3 explorer.db .dump | gzip -c > explorer.db.txt.gz
    • サイズ比較:
      • 元の SQLite データベース: 3.4GB
      • テキストダンプ: 1.3GB
      • gzip 圧縮テキスト: 240MB
    • gzip 圧縮版は元の SQLite データベースより14倍小さかった

実際の手順と安定性への効果

  • 新しい方法では、リモートサーバーで圧縮ダンプを作成し、そのファイルだけをダウンロードしてからローカルでデータベースを再構築する
  • コピー手順は次のとおり
    • サーバーで gzip 圧縮テキストファイルを作成: ssh username@server "sqlite3 my_remote_database.db .dump | gzip -c > my_remote_database.db.txt.gz"
    • ローカルへコピー: rsync --progress username@server:my_remote_database.db.txt.gz my_local_database.db.txt.gz
    • サーバー上の一時圧縮ファイルを削除: ssh username@server "rm my_remote_database.db.txt.gz"
    • ローカルで展開: gunzip my_local_database.db.txt.gz
    • ローカルデータベースを再構築: cat my_local_database.db.txt | sqlite3 my_local_database.db
    • ローカルのテキストファイルを削除: rm my_local_database.db.txt
  • 圧縮ダンプは一貫したコピー元として機能する
    • 大きなデータベースを長時間かけてコピーしている間に元データが更新されると、結果のファイルは前半が更新前、後半が更新後の状態になることがある
    • こうしたファイルはローカルで開くと database disk image is malformed エラーを出す可能性がある
  • コピー前にテキストダンプを作っておけば、rsync が転送するファイルは途中で変化しない
    • 完全で一貫したテキストファイルをコピーできる可能性が高まる
  • 大きな SQLite データベースを頻繁に移動するなら、この方法でダウンロード時間を短縮し、コピーの信頼性を高められる

1件のコメント

 
GN⁺ 2025-05-02
Hacker Newsの意見
  • 稼働中で更新を受けているデータベースファイルをそのままコピーすれば、当然破損する可能性がある
    SQLiteデータベースを安全に複製するならLitestreamがある: https://github.com/benbjohnson/litestream

    • Litestreamは普通のSFTP上でも動作するので、SSHでアクセスできるほぼあらゆるUNIXエンドポイントにデータベースのレプリケーションをストリーミングできる
      好みのサービスはあるが、どのSFTPサーバーでも可能
      [1] https://github.com/benbjohnson/litestream/issues/140
      [2] https://www.rsync.net/resources/notes/2021-q3-rsync.net_technotes.html
    • 「必ず」破損するという話には例外がある。BtrfsZFSでファイルまたはサブボリュームをアトミックにコピーすれば、ACIDデータベースやLSMツリーなら最悪でもロールバックが起きるだけ
      もちろん複数ファイルなら、すべてが同じトランザクション内でコピーされるようサブボリュームにまとめる必要があり、単にcp --reflink=alwaysするだけでは不十分
      SIGSTOPでプロセスを止めるのも似た結果は出せるかもしれないが、それに頼りたくはない
    • 組み込みの**.backupコマンド**も、稼働中のデータベースのスナップショット版を作ってコピーできるようにするための公式ツール
    • 自分のシステムではLitestreamを問題なく使っているが、組み込みツールだけで似た作業を手動で行う方法をかなり詳しく案内したこのドキュメントも良い: https://litestream.io/alternatives/cron/
    • Litestreamは本当に素晴らしい。休暇中に倉庫が浸水した、小さなKVMスタートアップを作った元Google社員がやっていたように、コンテナレベルでのSQLiteバックアップとリカバリに使ってみるつもり
      記憶が正しければ彼は完璧なガイドを書いていたが、見つけられる可能性はほぼゼロ。このリファレンスを知っている人がリンクを貼ってくれるとありがたい
  • コンピューター間でデータベースをコピーする話なのに、円だけ描いて残りのフクロウを省略している感じ
    他の人も言っているように増分rsyncのほうがずっと速いだろうが、もっと気になるのは、SQL文を送るほうがデータベースを送るより速いと言いながら、そのSQL文を実行しなければならない事実を完全に抜かしている点。その後に/optimize/も回し、/vacuum/も回す必要がある
    今まさにCSVファイルからデータベースを「増分再構築」しなければならない状況があるが、自分の場合は最初から作り直すほうが最適。それでもかなり最適化したにもかかわらず、空のメモリデータベースにバッチ挿入してインデックスを作るだけで30分かかる

    • https://stackoverflow.com/questions/1711631/improve-insert-per-second-performance-of-sqliteを見ているといいのだが
      SQLite3で高速に挿入する方法を非常によくまとめた記事
    • どんな最適化でも、ここではボトルネックがどこにあるかが重要。元記事のケースでは帯域幅がボトルネックで、CPU/ディスクI/Oは十分あるように見える
      250MBのデータベースをダウンロードするのに1分かかるとあったが、自分は1Gbpsの光回線のおかげで、会社のサーバーから2GBのSQLiteテストデータベースを15秒で取得できた
    • 30分は長く見える。データ量がとても多いのか? 大量のJSONデータでSQLiteデータベースをブートストラップする作業をしているが、値をリストに集めて1万件ずつ挿入すると、数百万行でも数分以内に入れられるかなり良い性能のスイートスポットを見つけた
      BloomフィルタやLRUキャッシュのようなテクニックを使う必要はあったが、今では6GBのデータベースを20分ほどで作れる
  • SQLiteにはこの用途の公式ツールがある: https://www.sqlite.org/rsync.html
    ページレベルで動作する
    「プロトコルは、レプリカが各ページの暗号学的ハッシュを元側に送り、元側はハッシュが一致しないページの全内容を送り返す、という方式である」

    • その通りだが、残念ながらSQLiteチームはそのツールを、ほとんどのディストリビューションやbrewがSQLiteのパッケージングに使うautotools tarballには含めていない
      このツールを使うには自分でコンパイルするしかない
  • 最近リリースされたsqlite_rsyncユーティリティは、SQLiteデータベースの内部構造に合わせて最適化したrsyncアルゴリズムのバージョンを使う。内部データページを効率よく比較したうえで、変更されたページまたは存在しないページだけを同期する
    記事のコツも良いが、今では組み込みユーティリティを使うほうが簡単 :)
    動作原理を詳しくブログに書いておいた: https://nochlin.com/blog/how-the-new-sqlite3_rsync-utility-works

    • sqlite3_rsyncは今ではrsync.netプラットフォームに組み込まれている
      ssh user@rsync.net sqlite3_rsync … blah blah …
      先週追加されたばかりで、まだ全リージョンには展開されていないが、初期ユーザーは全員、期待どおりに動作したと言っている
    • sqlite_rsyncWALモードでのみ使える。WALモードの追加制約として、データベースファイルはローカルディスク上になければならない
      たいていは当然そうするだろうが、それが不可能な場合はこのユーティリティを使えない
    • 頻繁に同期するなら、帯域幅のためにページサイズを大きくする必要がある
  • rsync が提供する転送中の圧縮を試していなかったのは意外だった
    -z, --compress は転送中にファイルデータを圧縮し、--compress-level=NUM は圧縮レベルを明示的に指定する
    おそらく gzip で圧縮してから転送するほうが速い場合もあるだろうが、フラグ1つで転送を改善できるのは良い点

    • 破損の問題に触れるなら、-z と一緒に sqlite3_rsynchttps://sqlite.org/rsync.html)を使うほうがよい
      SQLite のトランザクションと WAL を認識する rsync に、転送中の圧縮まで付くようなもの
    • 肝心なのは インデックス をスキップすることで、これは圧縮の前に行う必要がある
      こういう作業では、ダンプをそのまま gzip にストリーミングする。たいてい中間ファイルなしで宛先へ直接ストリーミングする方法も見つかる
      さらにこうすれば、宛先には圧縮された状態で保存される。貧者のレプリケーションではなくバックアップが目的なら、特にそうだ
    • 圧縮が有利なのは遅いネットワークだけだと思う
    • これは必ずやるべき。圧縮済みファイルに rsync を使うと、rsync を使う核心である ローリングチェックサムに基づく差分転送 を完全に迂回してしまう
    • --remove-source-files を使えば、もう一度 SSH で入って rm しなくてもよかったはず
  • テキストファイルとして保存するのは非効率。自分は SQLite データベースを VACUUM INTO で保存している
    sqlite3 -readonly /path/db.sqlite "VACUUM INTO '/path/backup.sqlite';"
    https://sqlite.org/lang_vacuum.html によると、INTO 節付きの VACUUM コマンドは、実行中のデータベースのバックアップコピーを作るバックアップ API の代替手段である。利点は、生成されるバックアップデータベースのサイズが最小化され、ファイルシステム I/O を減らせる可能性があること

    • かっこいい方法ではあるが、元記事で言っていた インデックス問題 は解決しない。遅いリンクでインデックスデータを送らないことが核心的なアイデアだったが、VACUUM INTO 方式ではインデックスが維持される
      テキストファイルはそのままでは非効率かもしれないが、gzip のような原始的なツールでもよく圧縮できる。SQLite のバイナリ形式も同じくらいよく圧縮できるかは確かではないが、可能性はある
    • その方式はインデックスを保持するのか? 元記事では SQLite ファイルが大きい理由は インデックス だと言っていた
  • DuckDB では同じことをしつつ Parquet にエクスポートでき、そうするとテキストベースの SQL 文よりデータサイズが一桁倍率で小さくなる。転送も速く、ロードも速い
    https://duckdb.org/docs/stable/sql/statements/export.html

    • コマンドラインではこうできる
      duckdb -c "attach 'sqlite-database.db' as db; copy db.table_name to 'table_name.parquet' (format parquet, compression zstd)"
      自分のテスト用データベースでは、gzip 圧縮したテキスト SQL 文より約20%小さかった
  • SQLite には、テーブル集合の変更を追跡し、以前のバージョンの SQLite データベースに適用できる changeset/patchset を作るセッション拡張がある
    https://www.sqlite.org/sessionintro.html

    • これをサポートする SQLite バインディングをまだ一つも見たことがない。なので、アプリケーションを C で書くか、言語バインディングをパッチする気がないなら、かなり使い道が限られる
      自分のプロジェクトの一つでは、すべての SQL 文とパラメータを別データベースに記録し、それを同期して再生する形で、貧者のセッションを自前実装した。毎日約0.1%だけ変わる 30GB 程度のデータベースには十分うまく動いている
    • 使ったことはある? ドキュメントは読んだが、この拡張を実際に使っている人の話は聞いたことがない
    • SQLite に、セッションや似た方式で 変更データキャプチャ を行う標準的な方法があると本当に良いのだが
  • 古いバージョンから新しいバージョンへ定期的に同期するなら、gzip の --rsyncable オプション でさらに最適化できる
    圧縮率は約1%低下するが、あるバージョンから次のバージョンへの差分が圧縮出力全体に連鎖せず、局所化される
    もう一つの方法は、ダンプ出力を圧縮せず、rsync に以前の非圧縮ダンプと現在のダンプの差分を計算させてから、ネットワークで送る変更集合だけを圧縮させること。つまり rsync -z を使えばよい

  • これは一つの パイプライン として処理できそう
    ssh username@server "sqlite3 my_remote_database.db .dump | gzip -c" | gunzip -c | sqlite3 my_local_database.db

    • SSH 呼び出しで -oCompression=on-C により SSH 圧縮 を有効にするなら、gzip/gunzip は重複かもしれない
    • 自分も最初に思いついたのはそれだった。そしてそこに pv が入るべきなのも、かなり明白に思える