課題: 知らないデータベースファイルを渡されたとき
四半期に 1 回くらい、自分が作っていない .db ファイルを前にします。ユーザーが旧アプリのエクスポートをアップロードしてきたり、SaaS ツールがバックアップを落としてきたり、同僚が「あのスクリプトの出力」を渡してきたり。
正直なワークフロー:
-
sqlite3 mystery.dbを開く -
.tables— まあ何かある -
.schema users—usersテーブルがあるらしい - 「データベース全体のサイズは?」 → ドキュメントを掘る
- 「最も行数が多いテーブルは?」 → テーブルごとにクエリを書く
- 「
user_idにインデックスはある?」 →.schema postsを目を凝らして見る
どれも PRAGMA か sqlite_master クエリか仮想テーブルクエリです。SQLite は驚くほどリッチなメタデータ面を公開していますが、PRAGMA table_info(x) と PRAGMA index_list(x) と dbstat と foreign_key_list の使い分けを誰も覚えていません。
sqlite-stats はこれらすべてを一発で実行してレポートを出力します。
📦 GitHub: https://github.com/sen-ltd/sqlite-stats
$ sqlite-stats mystery.db
=== Database ===
path : mystery.db
file size : 128.0 KB (32 pages × 4096 bytes)
encoding : UTF-8
journal mode : delete
user_version : 0 application_id: 0 foreign_keys: on
=== Tables (5) ===
name rows cols idx approx
comments 500 4 1 10.8 KB
posts 200 5 2 49.9 KB
users 50 4 1 1.4 KB
...
設計: 3 依存と bundled SQLite
clap = { version = "4.5", features = ["derive"] }
rusqlite = { version = "0.32", features = ["bundled"] }
serde_json = "1"
rusqlite の bundled は SQLite のアマルガメーションをバイナリに直接コンパイルします。ビルドウェイトが約 8 MB 増えますが、(1) 外部 libsqlite3 依存なしでどこでも動く、(2) bundled ビルドには SQLITE_ENABLE_DBSTAT_VTAB が有効になっている、という利点があります。システムの SQLite には通常 dbstat がないため、テーブルごとのバイト数を報告するにはこれが必要でした。
async なし。 短命な CLI がデータベースを開いて十数個の PRAGMA を読んで終了するだけ。async は純粋なセレモニーです。
最終 Alpine イメージ: 10.6 MB。
インスペクタは PRAGMA 呼び出しの集合
pub fn read_database_info(conn: &Connection) -> Result<DatabaseInfo> {
let page_size = pragma_i64(conn, "page_size")?;
let page_count = pragma_i64(conn, "page_count")?;
let encoding = pragma_string(conn, "encoding")?;
let journal_mode = pragma_string(conn, "journal_mode")?;
let user_version = pragma_i64(conn, "user_version")?;
let application_id = pragma_i64(conn, "application_id")?;
let foreign_keys_on = pragma_i64(conn, "foreign_keys")? != 0;
// ...
}
いつも忘れて調べ直すもの:
-
PRAGMA page_size×PRAGMA page_count= 実ファイルサイズ。stat()なしで計算でき、ネットワークや読み取り専用ファイルシステムで便利 -
PRAGMA database_listはアタッチされたデータベースを行で返す。カラム 2 がディスク上のパス -
PRAGMA user_versionとPRAGMA application_idはアプリがスキーマバージョンやファイルマジックを刻印する 32 ビットスロット。多くのアプリが使っているが、大半のダンプでは 0 のまま -
PRAGMA foreign_keysの厄介な性質: コネクションごとでデフォルトはオフ。レポートのforeign_keys: onはインスペクタのコネクションで有効かどうかであり、書き込み時に有効だったかではない
テーブルごとの情報は PRAGMA table_info(x)(列ごとの行)、PRAGMA index_list(x)(UNIQUE 制約や主キーの自動生成含む)、PRAGMA foreign_key_list(x) で取得します。
動的 PRAGMA 呼び出しのクォーティングには注意が必要です:
pub fn quote_ident(s: &str) -> String {
let escaped = s.replace('"', "\"\"");
format!("\"{escaped}\"")
}
これがないと、order という名前のテーブルでツールが壊れます。
dbstat: テーブルごとのバイト数
「Top 5 largest tables」セクションが、このツールを作った本当の理由です。PRAGMA page_count はファイル全体が 128 KB だと教えてくれますが、どのテーブルが大きいかは教えてくれません。それには dbstat 仮想テーブルが必要です:
SELECT name, SUM(payload) FROM dbstat GROUP BY name;
bundled ビルドの rusqlite にはこれが有効ですが、macOS のシステム SQLite や多くの Linux ディストロではデフォルトで無効です。dbstat が使えない場合は row_count * 64 バイトの粗い推定にフォールバックし、出力に (est) と表示します。
EXPLAIN QUERY PLAN をツリー表示
SQLite は EXPLAIN QUERY PLAN をフラット結果セット (id, parent, notused, detail) で返します。parent が id を参照する標準的な親ポインタツリーなので、PlanNode に変換してボックス描画グリフでレンダリングしました。
$ sqlite-stats demo.db --explain-query 'SELECT * FROM posts WHERE user_id = 1'
└─ SEARCH posts USING INDEX idx_posts_user (user_id=?)
マルチジョインクエリではインデント付きの実際のツリーが得られ、sqlite3 .expert のフラット出力よりもずっと読みやすいです。
3 つのフォーマッタ、同じデータ
Report 構造体が単一の真実源です。human(ターミナル表示)、json(プリティプリント)、markdown(PR コメントに貼れるテーブル)の 3 つのフォーマッタが消費します。最もよく使うのは markdown — 「送ってもらったデータベースの中身、レビュー用に」という Slack メッセージは驚くほど多い。
テスト
21 統合テストはすべて :memory: データベースを作成し、フィクスチャスキーマを投入してアサートします。テンプファイルなし、クリーンアップなし。ページサイズ、エンコーディング、テーブル検出、行数、NOT NULL/PRIMARY KEY フラグ、インデックス、外部キー、フォーマッタのヘッドラインセクション、EXPLAIN ツリーの親子関係まで検証しています。
トレードオフ
-
インタラクティブモードなし。 クエリ実行は
sqlite3の仕事。パイプで合成できるよう非インタラクティブ。 -
書き込みなし。
SQLITE_OPEN_READ_ONLYで開きます。VACUUMもuser_versionの更新もしない。安全に手が出せる。 -
ロック系 PRAGMA 回避。
wal_checkpointのように書き込みロックを取る PRAGMA は使わない。 -
ページ単位の分析なし。
dbstatからは未使用バイトやオーバーフローページ数も取れますが、レポート面を小さく保っています。
30 秒で試す
git clone https://github.com/sen-ltd/sqlite-stats
cd sqlite-stats
docker build -t sqlite-stats .
docker run --rm -v "$PWD":/work sqlite-stats /work/your.db
手元に SQLite ファイルがなければ、Firefox プロファイル(places.sqlite)、VS Code(state.vscdb)、macOS の iMessage(~/Library/Messages/chat.db)にあります。state.vscdb に向けると、VS Code の状態がどれだけ SQLite に住んでいるか実感できます。
次にやること
最も自然な追加はヒストグラムモード(カラム値のバケッティング)と --diff モード(2 データベースのスキーマ差分)です。どちらも Report 構造体の自然な拡張で小さく作れます。でも現状でも「この .db ファイルに何が入っている?」の 95% に 1 コマンドで答えられるのが目標でした。
PRAGMA が SQLite の隠し API なら、sqlite-stats はそれを実行できるチートシートです。
