1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

いつも忘れる SQLite の PRAGMA を CLI にまとめた

1
Last updated at Posted at 2026-05-03

課題: 知らないデータベースファイルを渡されたとき

四半期に 1 回くらい、自分が作っていない .db ファイルを前にします。ユーザーが旧アプリのエクスポートをアップロードしてきたり、SaaS ツールがバックアップを落としてきたり、同僚が「あのスクリプトの出力」を渡してきたり。

正直なワークフロー:

  1. sqlite3 mystery.db を開く
  2. .tables — まあ何かある
  3. .schema usersusers テーブルがあるらしい
  4. 「データベース全体のサイズは?」 → ドキュメントを掘る
  5. 「最も行数が多いテーブルは?」 → テーブルごとにクエリを書く
  6. user_id にインデックスはある?」 → .schema posts を目を凝らして見る

どれも PRAGMA か sqlite_master クエリか仮想テーブルクエリです。SQLite は驚くほどリッチなメタデータ面を公開していますが、PRAGMA table_info(x)PRAGMA index_list(x)dbstatforeign_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"

rusqlitebundled は 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_versionPRAGMA 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) で返します。parentid を参照する標準的な親ポインタツリーなので、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 で開きます。VACUUMuser_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 はそれを実行できるチートシートです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?