接続とは、ただファイルを開くこと
SQLite にサーバーは存在しません。ポートで待ち受けるデーモンも、接続先のホストも、認証情報のやり取りもありません。「接続する」とは、ドライバがディスク上のファイルを開いて、そのページを読み書きし始めることを意味します。これがすべてです。
どの言語にも、SQLite の C ライブラリをラップしたドライバが用意されています。形は違えど、登場人物は同じです。データベースファイルのパス、それを開く関数、SQL を実行するためのハンドル、そして使い終わったら呼ぶクローズ処理。これだけです。
-- 概念的には、すべてのドライバは以下を実行します:
-- 1. 指定されたパスでファイルを開くか、作成する。
-- 2. ハンドルを取得する。
-- 3. プリペアドステートメントを介してSQLを実行する。
-- 4. ハンドルを閉じる。
このページの残りでは、それを実際のコードに落とし込みつつ、最初のクエリを投げる前に設定しておきたい数少ない項目を紹介します。
Python での SQLite 接続:標準ライブラリの sqlite3
Python には sqlite3 が標準で付いてくるので、追加インストールは不要です。基本の形はこんな感じです。
-- Python
import sqlite3
conn = sqlite3.connect("app.db")
conn.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
conn.execute("INSERT INTO notes (body) VALUES (?)", ("最初のメモ",))
conn.commit()
for row in conn.execute("SELECT id, body FROM notes"):
print(row)
conn.close()
知っておきたいポイントをいくつか挙げておきます。
sqlite3.connect("app.db")は、ファイルが存在しなければ自動で作成されます。":memory:"を渡せば、RAM 上だけに存在するデータベースになります。sqlite3.connect("file:app.db?mode=ro", uri=True)のように URI 形式で書くと、読み取り専用で開けます。- SQL 中の
?はプレースホルダーです。文字列結合ではなく、必ずパラメータバインディングを使ってください。詳しくは次の章で扱います。 conn.commit()は明示的に呼ぶ必要があります。ただし、コンテキストマネージャ(with conn:)を使えば自動でコミットされます。
長時間動かすアプリでは、複数の書き込みが競合したときにエラーにせず待機させるため、ビジータイムアウトを設定しておきましょう。
-- Python
conn.execute("PRAGMA busy_timeout = 5000") -- 最大5秒待機
conn.execute("PRAGMA journal_mode = WAL") -- 並行性が向上
Node.js: better-sqlite3
Node のエコシステムにはいくつか選択肢がありますが、現場のチームでまず候補に挙がるのが better-sqlite3 です。同期APIなので「Nodeなのに同期?」と一瞬戸惑うかもしれませんが、SQLiteのクエリはマイクロ秒単位で返ってくるので、むしろこちらの方が速いんです。
-- Node.js
const Database = require("better-sqlite3");
const db = new Database("app.db");
db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
const insert = db.prepare("INSERT INTO notes (body) VALUES (?)");
insert.run("最初のメモ");
const rows = db.prepare("SELECT id, body FROM notes").all();
console.log(rows);
db.close();
db.prepare(...) は再利用可能なステートメントオブジェクトを返します。書き込みには .run()、全行取得には .all()、1行だけ取りたいときは .get() を使います。だいたいの SQL ドライバと同じ書き方なので、すぐ馴染めると思います。
起動時に PRAGMA を設定しておきましょう:
-- Node.js
db.pragma("journal_mode = WAL");
db.pragma("busy_timeout = 5000");
db.pragma("foreign_keys = ON"); -- デフォルトはオフだが、ほぼ常に有効にしたい
foreign_keys = ON はぜひ覚えておきたいポイントです。SQLite は明示的に指定しない限り外部キー制約を強制してくれません。しかも 接続ごと に設定が必要です。これを忘れると、REFERENCES 句はただの飾りになってしまいます。
Go から SQLite に接続する: database/sql とドライバ
Go の標準 database/sql パッケージはドライバ非依存の作りになっています。SQLite 用ドライバとしては、modernc.org/sqlite(ピュア Go、CGO 不要)と github.com/mattn/go-sqlite3(CGO ベース)の 2 つがよく使われています。
-- Go
import (
"database/sql"
_ "modernc.org/sqlite"
)
db, err := sql.Open("sqlite", "app.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)")
if err != nil { panic(err) }
defer db.Close()
_, err = db.Exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
_, err = db.Exec("INSERT INTO notes (body) VALUES (?)", "最初のメモ")
rows, _ := db.Query("SELECT id, body FROM notes")
defer rows.Close()
for rows.Next() {
var id int; var body string
rows.Scan(&id, &body)
fmt.Println(id, body)
}
ファイル名の後に付けるクエリ文字列は、このドライバが接続時に PRAGMA を渡すための仕組みです。書き方はドライバによって違うので、使うドライバのドキュメントを必ず確認してください。
sql.Open を呼んだ時点では実際には接続が張られず、最初のクエリを実行したタイミングで接続されます。db はコネクションプールです。SQLite の場合、プールは小さめに保つのが基本で、書き込みが多いワークロードでは db.SetMaxOpenConns(1) にしてしまうのが無難です。
Java から JDBC で SQLite に接続する
定番のドライバは org.xerial:sqlite-jdbc です。JDBC の接続文字列は jdbc:sqlite:<path> の形式になります:
-- Java
import java.sql.*;
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:app.db")) {
try (Statement st = conn.createStatement()) {
st.execute("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
st.execute("PRAGMA journal_mode = WAL");
st.execute("PRAGMA busy_timeout = 5000");
}
try (PreparedStatement ps = conn.prepareStatement("INSERT INTO notes (body) VALUES (?)")) {
ps.setString(1, "最初のメモ");
ps.executeUpdate();
}
try (PreparedStatement ps = conn.prepareStatement("SELECT id, body FROM notes");
ResultSet rs = ps.executeQuery()) {
while (rs.next()) System.out.println(rs.getInt(1) + " " + rs.getString(2));
}
}
インメモリの場合は jdbc:sqlite::memory: を指定します。読み取り専用にしたいときは ?open_mode=1 を末尾に付けるか、SQLiteConfig オブジェクトを使ってください。
PHP: PDO で SQLite に接続する
PHP の PDO で SQLite を扱う場合、DSN の形式は sqlite:<path> になります。
-- PHP
$db = new PDO("sqlite:app.db");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
$db->exec("PRAGMA journal_mode = WAL");
$db->exec("PRAGMA busy_timeout = 5000");
$stmt = $db->prepare("INSERT INTO notes (body) VALUES (?)");
$stmt->execute(["最初のメモ"]);
foreach ($db->query("SELECT id, body FROM notes") as $row) {
echo $row["id"] . " " . $row["body"] . "\n";
}
sqlite::memory: を指定すればインメモリのデータベースになります。ATTR_ERRMODE は必ず例外モードにしておきましょう。サイレントに失敗されるとデバッグが本当に辛いです。
接続文字列とファイルパス
どのドライバでも、「接続文字列」には大きく分けて2つの書き方があります。
- 素のパス:
app.db、./data/app.db、/var/lib/myapp/app.dbのような形式です。相対パスはプロセスのカレントディレクトリ基準で解決されますが、本番環境ではまず期待通りに動きません。絶対パスを使うのが無難です。 - URI形式:
file:app.db?mode=rwc&cache=sharedのような書き方です。mode=ro(読み取り専用)、mode=rwc(読み書き+なければ作成、デフォルト)、cache=shared、nolock=1などのフラグを指定できます。
特殊な値もいくつか覚えておくと便利です。
:memory:— プライベートなインメモリDB。コネクションごとに別物になります。file::memory:?cache=shared— 同一プロセス内の複数コネクションで共有できるインメモリDB。""(空文字列) — クローズ時に削除される、プライベートな一時的ディスクDB。
JDBC では URI の前に jdbc:sqlite: を付けます。PDO では sqlite: です。Go のドライバや Python の sqlite3 は、パスでも URI でもそのまま受け付けてくれます。
コネクションプールはどうする?
SQLite は「シングルライター」のデータベースです。ある瞬間に書き込みロックを握れるコネクションは1つだけで、他は順番待ちになります。ライターをいくらプールに積んでも書き込みは速くなりません。同じロックを奪い合う相手が増えるだけです。
とはいえ、小さめのプールが役立つ場面もあります。
- WAL モードでの並行読み取り。リーダー同士、そしてリーダーとライターは互いをブロックしません。
- ヘッドオブラインブロッキングの回避。重いクエリが1本走ってもアプリ全体が止まらないようにできます。
Web アプリで扱いやすい設定の目安はこんなところです。
- WAL モードを有効化(
PRAGMA journal_mode=WAL)。 busy_timeoutを数秒に設定し、競合時はエラーで落とさず素直に待つ。- プールサイズはライター1 + リーダーN、トラフィックが軽ければ共有コネクション1本でも十分。
- 外部キー制約はコネクションごとに必ず有効化する。
-- これらを新しい接続ごとに適用します:
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = ON;
PRAGMA synchronous = NORMAL; -- WAL では安全; FULL より高速
synchronous = NORMAL は WAL と組み合わせる定番の設定です。アプリのクラッシュには耐えられて、OS レベルのクラッシュに対してはやや緩めですが、デフォルトの FULL よりも体感できるくらい高速になります。
接続のクローズ処理とその重要性
どのドライバにもクローズ用のメソッドが用意されています。conn.close()、db.Close()、db.close() といった具合です。これを呼び忘れるとファイルディスクリプタがリークし、WAL ファイルが肥大化していく原因にもなります。
長時間動かすサービスでよく使われるのは、リクエストごとに開いて閉じるのではなく、プロセスの生存期間を通して 1 本の接続(またはプール)を使い回す パターンです。SQLite の接続を開く処理自体は軽いのですが、毎回 PRAGMA を設定し直すのは無駄ですし、設定漏れも起こりやすくなります。
-- Python — プロセスごとに接続を作成し、リクエスト間で再利用
DB = sqlite3.connect("app.db", check_same_thread=False)
DB.execute("PRAGMA journal_mode = WAL")
DB.execute("PRAGMA busy_timeout = 5000")
DB.execute("PRAGMA foreign_keys = ON")
Python の場合、複数スレッドから同じコネクションを使うなら check_same_thread=False を指定する必要があります。ただしその場合は、呼び出しを直列化するためのロックかコネクションプールを併用しましょう。
本番投入前のチェックリスト
実トラフィックを SQLite データベースに向ける前に、以下を確認しておきましょう。
- データベースファイルのパスは絶対パスで指定する。
- WAL モードを有効にする(
PRAGMA journal_mode = WAL)。 busy_timeoutを 2〜10 秒に設定する。- 外部キーは接続ごとに毎回 ON にする。
- プレースホルダ(パラメータバインディング)付きのプリペアドステートメントを使う。文字列連結は絶対に避ける。
- データベースを置くディレクトリにプロセスから書き込めることを確認する(WAL モードでは本体ファイルと並んで
-walと-shmファイルが作られます)。 - バックアップは必要になる前に検討しておく。
VACUUM INTOと.backupコマンドについては後の章で扱います。
次のステップ:マイグレーション
接続するところまでは簡単です。難しいのは、本番データベースを手作業で書き換えずにスキーマを進化させていく部分です。マイグレーションを使えば、ALTER TABLE を再現可能でバージョン管理されたプロセスに落とし込めます。次のページで詳しく見ていきましょう。
よくある質問
コードからSQLiteデータベースに接続するには?
ドライバにファイルパスを渡すだけでOKです。Pythonなら sqlite3.connect('app.db')、Nodeは better-sqlite3 を使って new Database('app.db')、Goなら sql.Open("sqlite", "app.db") という具合です。SQLiteにはサーバーがないので、「接続」と言ってもファイルを開いているだけ。ファイルが存在しなければSQLiteが自動で作ってくれます。
SQLiteの接続文字列はどう書くの?
ほとんどのドライバは普通のファイルパス (./data/app.db) かURI形式 (file:app.db?mode=rwc&cache=shared) のどちらでも受け付けます。URI形式にすると、読み取り専用モード、共有キャッシュ、:memory: データベースといったフラグも指定可能です。JDBCなら jdbc:sqlite:app.db、PDOなら sqlite:app.db という書き方になります。
SQLiteでもコネクションプールは必要?
PostgresやMySQLと同じ感覚で必要、というわけではありません。SQLiteは書き込みをDBレベルで直列化するため、書き込み用のプールを作っても速くなりません。ただ、特にWALモードでは並行読み取りに小さなプールが効くこともあります。実際のところ、共有接続を1つだけ持って PRAGMA journal_mode=WAL と適切な busy_timeout を設定するだけで十分動くアプリは多いです。
「database is locked」エラーを防ぐには?
まずビジータイムアウトを設定して、即座に失敗せず待たせるようにします:PRAGMA busy_timeout = 5000(ミリ秒)。さらに PRAGMA journal_mode=WAL でWALモードを有効にすれば、読み手が書き手をブロックしなくなります。あとはトランザクションを短く保ち、書き込みトランザクションを開いたままDB以外の重い処理を走らせないこと。これだけでかなり減らせます。