mcpField
S
sqlite-ops

sqlite-ops

github.com/0xdarkmatterデータ・データベース78105 回インストール2 週間前に更新
リポジトリ

sqlite-ops について

クエリ性能、並行処理、スキーマ設計、ホストごとの差異、保守を扱う SQLite 運用・開発向けスキルです。遅いクエリやロック、整合性の問題の調査に役立ち、EXPLAIN QUERY PLAN を分類するスクリプトも含みます。

主な機能

  1. 01EXPLAIN QUERY PLAN による調査
  2. 02インデックス設計と性能測定のガイド
  3. 03WAL、ロック、並行処理への対応
  4. 04スキーマ制約と移行方法の解説
  5. 05クエリ計画を分類するスクリプト

活用シーン

  1. 01遅いクエリや全表スキャンの診断
  2. 02SQLITE_BUSY や書き込み待ちへの対処
  3. 03バックアップや整合性確認手順の選択

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/0xdarkmatter/claude-mods --skill sqlite-ops

インストール後、Claude Code で/sqlite-opsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

assets/
.gitkeep
references/
async-patterns.md
concurrency-durability.md
covering-index-worked-example.md
d1-edge.md
d1-production-patterns.md
eqp-triage.md
feature-modules.md
hosts.md
migration-patterns.md
operations.md
query-performance.md
schema-design.md
schema-patterns.md
testing.md
scripts/
.gitkeep
eqp-triage.py
tests/
run.sh

詳細

SQLite の操作

SQLite は、さまざまなホストで利用できる単一のエンジンです。sqlite3 CLI、Python、node:sqlite、better-sqlite3、Bun、Cloudflare D1、libSQL/Turso のいずれを通じて利用しても、SQL のセマンティクス、クエリプランナー、プラグマは同じです。異なるのは、ドライバーのインターフェースと運用環境(ファイルの所有者、「接続」にかかるコスト、PRAGMA を実行できるかどうかなど)です。まずエンジンについて考え、そのうえでホストごとのセクションを確認し、異なる注意点を把握してください。

Where does the problem live?
│
├─ A statement is slow, or scans too much
│  └─ EXPLAIN QUERY PLAN first, always → references/query-performance.md
│
├─ "database is locked" / SQLITE_BUSY / writers blocking readers
│  └─ WAL + busy_timeout + BEGIN IMMEDIATE → references/concurrency-durability.md
│
├─ Wrong data got in, or a constraint didn't fire
│  └─ Type affinity, STRICT, foreign_keys=OFF → references/schema-design.md
│
├─ Search / JSON / geo / analytics feature question
│  └─ FTS5, JSON, R-tree, window fns → references/feature-modules.md
│
├─ Running on a managed/edge engine (D1, Turso)
│  └─ references/d1-edge.md + references/hosts.md
│
└─ Corruption, size, backup, VACUUM
   └─ references/operations.md

計測の心得(最適化を始める前にお読みください)

SQLite の「最適化」の多くは、計測されていません。次の 4 つのルールを、破られる頻度が高い順に示します。

  1. ツール呼び出しではなく、ステートメントを計測してください。 すでに別のクエリを送信しているバッチに含まれる高コストな集約処理は、追加のラウンドトリップが発生しないため、呼び出し単位の計測では見えません。それでも、リクエストごとにテーブル全体をスキャンしています。複数の部分からなるステートメントは分解し、それぞれを個別に計測してください。
  2. レイテンシとスキャンした行数の両方を報告してください。 この 2 つは独立して変化します。最適化によってレイテンシが約 25 倍短縮されても、読み取り行数はほとんど変わらないことがあります(D1 のような従量課金エンジンでは、読み取り行数がコストを左右する指標です。references/d1-edge.md を参照してください)。
  3. CLI の実時間を信用しないでください。 プロセスの起動時間が大部分を占めます。エンジンが報告する時間(CLI の .timer on、D1 の meta.timings.sql_duration_ms)を使ってください。
  4. 10 回以上実行し、中央値と範囲を報告してください。 初回の実行はコールド状態です。ある計測セッションでは、同じステートメントで中央値が 171 ms だったのに対し、コールド状態の実行は 2,495 ms でした。数千行を読み取る場合、初回に 1.5~1.7 倍の時間がかかることは珍しくありませんでした。
# sqlite3 CLI: engine-reported timing, not shell time
sqlite3 app.db '.timer on' "SELECT count(*) FROM q_product WHERE org LIKE '%acme%';"

# What the planner thinks the data looks like (empty = ANALYZE never ran)
sqlite3 app.db 'SELECT * FROM sqlite_stat1;'
インデックスを作成する前に、効果を確かめる

このスキルで最も効果が大きく、デプロイゲート内でスキーマ作業を完結させる方法です。既存のインデックスがカバーするカラムに対して、同じ形式のステートメントを実行します。 テーブル、行数、述語の形式は同じにし、カラムだけを変更します。その差が、本番データを使ってスキーマへの書き込みを一切行わずに計測した、予想される効果です。

-- Hypothesis: a covering index on (org, product_id) makes this fast.
-- Unindexed control (what you have today):
SELECT DISTINCT product_id FROM q_product WHERE org LIKE '%acme%';

-- Proof shot: same shape, over a column an existing index already covers.
-- If this is fast, the index is worth writing. If it isn't, the index is not your problem.
SELECT DISTINCT org FROM q_product WHERE org LIKE '%acme%';

実例では、効果を確かめるための実行結果は 6.75 ms、比較対象は 171.83 ms でした。本番スキーマに手を加えずに、インデックスを作成する根拠として十分な差です。

EXPLAIN QUERY PLAN — 60 秒で読み解く

遅いステートメントを調べるときは、まず EXPLAIN QUERY PLAN(EQP)を実行してください。低コストで安全な読み取り専用コマンドであり、任意の SQL を実行できるすべてのホストで利用できます。

EXPLAIN QUERY PLAN
SELECT DISTINCT product_id FROM q_product WHERE org LIKE '%acme%';
プランの行意味評価
SEARCH t USING INDEX ix (col=?)B ツリーで検索し、一致する行だけにアクセスします最良
SEARCH t USING COVERING INDEX ixインデックスだけで検索し、必要なカラムがすべて含まれているため、テーブルは読み取られません最良
SCAN t USING COVERING INDEX ix全体を走査しますが、幅の広い行ではなく、コンパクトなインデックスエントリを走査します多くの場合問題ありません。以下を参照
SCAN t USING INDEX ixインデックス全体を走査し、該当する各行を取得します要注意:インデックスの効果がほとんどありません
SCAN tテーブル全体をスキャンしますテーブルが非常に小さい場合を除き、改善してください
USE TEMP B-TREE FOR ORDER BY順序を提供するインデックスがないため、ソートしていますコストの兆候
USE TEMP B-TREE FOR GROUP BYグループ化のために同様の処理をしていますコストの兆候
CORRELATED SCALAR SUBQUERY外側の行ごとにサブクエリを再実行します通常、問題の根本原因です

最も重要な違い: SCAN … USING COVERING INDEX は失敗ではありません。カバリングスキャンは、幅の広い行をメモリに読み込む代わりに、コンパクトなインデックスエントリを読み取ります。これは、インデックス検索できない述語を高速化する方法そのものです。

詳細: ./references/query-performance.md — インデックス設計、カラムの順序、部分インデックスと式インデックス、ANALYZE/sqlite_stat1、およびクエリプランナーの最適化を妨げる要因の全一覧。

インデックスを使えない述語の落とし穴(実例)

このアドバイスの根拠となったD1の実測例(LIKE '%x%' に通常のインデックスを作成しても何も変わらない理由、修正方法、修正前後の数値)と、sqlite_stat1 の有無によるプランナーの確認については、references/covering-index-worked-example.md を参照してください。

インデックス設計一覧

述語の形インデックス化可能か作成するもの
col = ?、col IN (…)、col > ?、BETWEENはいcol の B-tree
a = ? AND b = ?はい複合インデックス (a, b) — 等価条件の列を先にする
a = ? ORDER BY bはい複合インデックス (a, b) — 一時 B-tree を不要にする
col LIKE 'x%'(前方一致)col が BINARY 照合順序の TEXT 型なら可能col の B-tree
col LIKE '%x%'(先頭にワイルドカード)シーク不可テーブルスキャンをカバリングするか、FTS5 のトライグラムを使う
lower(col) = ?通常のインデックスでは不可式インデックス ON t(lower(col))
行の 2% に該当する status = 'open'はい部分インデックス WHERE status = 'open'
json_extract(doc,'$.k') = ?通常のインデックスでは不可式インデックス、または生成列とインデックス

効果のあるルール: カバリングインデックスでは、フィルター対象の列を先に、射影対象の列を後に配置します。関数ではなく列にインデックスを作成します(式インデックスの場合を除く)。追加するインデックスはすべて書き込みの負担になるため、追加前に確認してください。

同時実行性と耐久性 — 80/20

症状原因対処方法
SQLITE_BUSY別の接続がロックを保持しており、この接続は待機をあきらめましたPRAGMA busy_timeout = 5000; を設定し、書き込みトランザクションを短く保つ
SQLITE_LOCKED同じ接続内(または共有キャッシュ内)での競合コードを修正してください — 再試行ループは永久に回り続けます
トランザクション中に「database is locked」後で書き込みを行う BEGIN(DEFERRED)の読み取り → アップグレード時のデッドロック。再試行できません書き込みを行うトランザクションには BEGIN IMMEDIATE を使う
書き込み中に読み取りがブロックされるロールバックジャーナルモードPRAGMA journal_mode = WAL;(設定は一度だけ。設定は保持されます)
-wal ファイルが際限なく大きくなる長時間実行される読み取りがチェックポイントを妨げています読み取りを終了または更新し、PRAGMA wal_checkpoint(TRUNCATE); を実行する
PRAGMA journal_mode = WAL;      -- persistent; survives reconnect
PRAGMA busy_timeout = 5000;     -- per-connection; set on EVERY connection
PRAGMA foreign_keys = ON;       -- per-connection, OFF by default — see below
PRAGMA synchronous = NORMAL;    -- safe with WAL; FULL only if you fear power loss

詳しくはこちら: ./references/concurrency-durability.md — WAL の内部動作、DEFERRED のアップグレード時のデッドロック、synchronous の各レベル、チェックポイントの停滞、複数プロセスからのアクセス。

スキーマ — 気づきにくい3つのバグ

  1. PRAGMA foreign_keys はデフォルトで OFF です。 接続ごとに設定が必要です。REFERENCES 句は解析され、保存されますが、何も行いません。これは SQLite アプリケーションでよくある、気づきにくいデータ整合性のバグです。
  2. 型アフィニティは型そのものではありません。 TEXT 列には整数も問題なく格納できます。宣言された型は、変換方法に関する指針です。宣言した型を強制したい場合は、STRICT テーブル(SQLite 3.37+)を使用してください。
  3. ALTER TABLE には制限があります。 列の追加と名前の変更はサポートされていますが、削除、型の変更、制約の変更には、12ステップの再作成手順が必要です。
CREATE TABLE product (
    id       INTEGER PRIMARY KEY,
    org      TEXT NOT NULL,
    price    REAL NOT NULL,
    doc      TEXT,
    -- indexable projection of a JSON field
    sku      TEXT GENERATED ALWAYS AS (json_extract(doc, '$.sku')) VIRTUAL
) STRICT;

詳しくはこちら: ./references/schema-design.md(アフィニティ、STRICT、生成列、WITHOUT ROWID、制約)と ./references/migration-patterns.md(12ステップの ALTER 手順、バージョン管理されたマイグレーションランナー)。

機能モジュールの概要

ニーズ使用するもの備考
部分文字列/あいまいテキスト検索trigram トークナイザーを使った FTS5大規模環境での LIKE '%x%' に対する実質的な解決策です
単語/フレーズ検索とランキングFTS5 + bm25()外部コンテンツテーブルを使えばコーパスの重複を避けられます
半構造化ドキュメントjson_extract / -> / ->>、JSONB(3.45+)生成列または式インデックスでインデックスを作成します
バウンディングボックス/区間の重なりR-tree 仮想テーブルコンパイル時のモジュールです。利用可能か確認してください
累計、ランキング、ギャップウィンドウ関数(3.25+)PostgreSQL と同じ構文です
挿入または更新ON CONFLICT … DO UPDATE(3.24+)excluded.col は挿入候補の行を参照します
書き込んだデータの読み戻しRETURNING(3.35+)アトミックなジョブ取得パターンを単一ステートメントで実現できます

詳細:./references/feature-modules.md。

ホスト

エンジンは同じですが、接続の仕組みは異なります。

ホストごとの接続モデルと注意点:ホストの概要。D1 ではコストは読み取り行数で、バインドパラメーターの上限は 100 です。また、PRAGMA は利用できません。

詳細:ホストごとの接続方法と注意点については、./references/hosts.md を参照してください。

特に D1 では、標準の SQLite に相当するものがなく、見落とされやすいプラットフォーム機能が 3 つあります。

wrangler d1 insights <db> --sort-type=sum --sort-by=reads --limit=10   # rank REAL queries by cost
wrangler d1 time-travel info <db>                                      # 30-day point-in-time restore point
# Sessions API (env.DB.withSession(bookmark)) - read replicas, sequential consistency

./references/d1-edge.md では、これらに加えて、読み取り行数に基づくコスト、検証済みの制限事項一覧、エラーカタログ、インポート/エクスポートを解説しています。本番環境でのインシデントパターン、つまりタイムアウトした migrations apply --remote が実際には適用されていたケース、batch() が対象行 0 件のスコープ付き UPDATE を成功として扱うケース、読み取りレプリケーションを有効化する段階的なロールアウト方法については、./references/d1-production-patterns.md を参照してください。

運用

sqlite3 app.db 'PRAGMA quick_check;'        # fast structural check
sqlite3 app.db 'PRAGMA integrity_check;'    # full check — slow on big DBs
sqlite3 app.db "VACUUM INTO 'backup.db';"   # consistent backup, no downtime, defragmented
sqlite3 app.db '.dump' > backup.sql         # portable text backup
sqlite3 app.db 'PRAGMA optimize;'           # run before closing a long-lived connection

書き込み中のライブデータベースファイルを cp でコピーすることは絶対に避けてください。VACUUM INTO、バックアップ API、または .dump を使用してください。

詳細:./references/operations.md — 破損の原因と復旧、VACUUM と VACUUM INTO の比較、ページ/キャッシュのサイズ設定、サイズ分析。

トリアージスクリプト

scripts/eqp-triage.py は EXPLAIN QUERY PLAN の結果を読み取り、データベースに対してステートメントを実行するか、パイプで渡されたプランテキストを受け取り、各行を修正のヒントとともに重大度別に分類します。該当箇所が見つかった場合(ドメインシグナル)は終了コード 10 を返し、プランに問題がない場合は 0 を返します。

# Run against a database file (uses Python's bundled sqlite3 — no external binary needed)
python3 scripts/eqp-triage.py --db app.db \
  --sql "SELECT DISTINCT product_id FROM q_product WHERE org LIKE '%acme%'"

# More: triage a plan piped from D1 or a log, and --json output -> references/eqp-triage.md

注意点

誤り問題になる理由対処法
LIKE '%x%' 用にインデックスを追加する先頭にワイルドカードがあるとインデックス検索できませんカバリングインデックス、または FTS5 の trigram を使います
シェルのストップウォッチで時間を計るCLI/ドライバーの起動時間が支配的になりますエンジンが報告する時間を使い、10 回以上の中央値を取ります
ステートメントではなくツール呼び出しの時間を計るまとめて実行されるステートメントが見えません分解して各部分の時間を計ります
REFERENCES が強制されると思い込む接続ごとに foreign_keys が OFF です接続のたびに PRAGMA foreign_keys = ON を実行します
宣言した型が強制されると思い込む型付けではなく型アフィニティですSTRICT テーブルを使います
SQLITE_LOCKED を再試行する同一接続内の競合は解消されませんコードパスを修正します
BEGIN の後に書き込むDEFERRED から書き込みへの昇格はデッドロックし、再試行できませんBEGIN IMMEDIATE を使います
ライブデータベースに cp を実行する不完全なコピーになりますVACUUM INTO/バックアップ API を使います
SELECT * を使うカバリングインデックスの効果が失われ、読み取る各行の幅が広がります必要な列だけを指定します
「速度向上」のために VACUUM を実行するファイル全体を書き換え、2 倍の空き容量が必要になり、ロックを保持しますPRAGMA optimize/対象を絞ったインデックス作業を行います
1 回の低速実行だけを信頼する初回実行に 1.5~1.7 倍のペナルティが発生するのはよくあることです10 回以上の中央値を取り、範囲も報告します
パラメーター上限を回避するためにリテラルを埋め込むそれがインジェクションの原因になります作業を分割し、バインドパラメーターを使い続けます
タイムアウトしたリモートマイグレーションを再実行する適用済みの可能性があり、エラーは応答に関するものですまず読み取り専用でスキーマの状態を確認します — ./references/d1-production-patterns.md
コミット済みの batch() をステートメントごとの成功とみなす条件付き UPDATE が 0 行に一致してもエラーにはなりませんmeta.changes を確認します。スコープ付き書き込みで 0 件なら 403/競合です

参照ファイル

参照先読み込むタイミング
./references/query-performance.md遅いステートメントに関する内容:EQP、インデックス設計、ANALYZE、プランナーが期待どおりに動作しない場合、測定方法
./references/d1-edge.mdCloudflare D1:読み取り行数に基づく料金、d1 insights、Sessions API/レプリケーション、Time Travel、制限、エラー
./references/d1-production-patterns.mdD1 の本番運用:タイムアウトしたマイグレーションの検証、batch() における 0 行書き込みの検証、レプリカへのオプトイン方式によるレプリケーションの段階的導入
./references/concurrency-durability.mdロック、WAL、busy_timeout、トランザクションモード、チェックポイント、耐久性
./references/schema-design.mdアフィニティ、STRICT、外部キー、生成列、WITHOUT ROWID、制約
./references/schema-patterns.mdすぐに使えるテーブル設計:状態、キャッシュ、イベントログ、キュー、セッション、重複排除
./references/migration-patterns.mdバージョン管理されたマイグレーション、12 ステップの ALTER 手順、ホスト固有のランナー
./references/feature-modules.mdFTS5、JSON/JSONB、R-tree、ウィンドウ関数、upsert、RETURNING
./references/hosts.mdホストごとの接続手順とドライバー特有の注意点(Python、Node、Bun、D1、libSQL)
./references/async-patterns.mdPython の aiosqlite を詳しく解説:非同期 CRUD、バッチ処理、プーリング
./references/operations.md整合性チェック、破損からの復旧、VACUUM、バックアップ、サイズとページのチューニング
./references/testing.mdインメモリデータベースとファイルデータベース、フィクスチャ、決定的なシードデータの投入、マイグレーションのテスト
./references/covering-index-worked-example.md上記の推奨事項の根拠となった、D1 のカバリングインデックスの測定例。統計情報の有無によるプランナーの比較
./references/eqp-triage.mdeqp-triage.py のその他の呼び出し例:D1 またはログからパイプで渡したプラン、--json 出力

関連項目

スキル組み合わせるタイミング
sql-opsベンダーに依存しない SQL:CTE、ウィンドウ関数、JOIN 戦略
perf-opsより広範なパフォーマンスワークフロー:プロファイリング、負荷テスト、実施前後の手順
cloudflare-opsD1 データベース周辺の Workers、バインディング、デプロイ
postgres-opsワークロードが SQLite の単一ライターモデルでは対応しきれなくなった場合
python-database-opsSQLite 上の SQLAlchemy/ORM レイヤー

コメント