株式会社ホコサキ

SQLite本番運用の3つの壁とlibSQL(Turso)への移行コスト

天京祐輔
天京祐輔
SQLite本番運用の3つの壁とlibSQL(Turso)への移行コスト

SQLiteを本番環境で使い始めると、最初のうちは何も問題が起きない。ファイル1つで完結する手軽さ、ゼロコンフィグの快適さ、十分すぎるほどの読み取り速度。「これで行けるじゃないか」という感触が続く。

ところが、ある閾値を超えた瞬間に壁が来る。同時アクセスが増えた、テナントが増えた、可用性を担保しなければならなくなった。そのタイミングで「SQLiteの限界」という言葉が頭をよぎる。

限界が具体的にどこで・なぜ起きるかを整理したうえで、libSQLベースのTurso SDKへの移行コストがどれほど低いかを実際のコード差分で確認する。「PostgreSQLに移行するほどでもないが、素のSQLiteのままでは不安」という中間地点にいるエンジニアに向けた内容だ。

SQLiteを本番で使い続けると、どこで詰まるか

「SQLiteは本番に向かない」という言説は雑すぎる。問題はもう少し具体的で、だいたい3つのパターンに収束する。

壁①:同時書き込みロック。WALモードにすることで読み取りの並列性は高められる。読み取りと書き込みが互いをブロックしなくなるため、読み取り中心のワークロードでは大きく改善する。しかし書き込みについては話が別で、WALモードにしても書き込みトランザクションは同時に1つしか実行できない。複数のプロセスやスレッドが同時に書き込もうとすると、待機中のプロセスは SQLITE_BUSY を受け取る。リトライロジックで吸収できる範囲もあるが、書き込みが増えるにつれてレイテンシの悪化は避けられない。

壁②:レプリケーション不在。SQLiteはファイルベースのデータベースであり、標準でレプリケーションの仕組みを持たない。読み取りをスケールアウトしたければ、Litestream のような外部ツールでWALをS3等にストリーミングするか、ファイルをコピーして別サーバーに配置するしかない。「できなくはない」が、構成の複雑さを自前で管理することになる。

壁③:マルチテナントのファイル管理爆発。テナントごとにデータを分離したい場合、SQLiteでよく使われるパターンは「テナントごとに .db ファイルを作る」というものだ。最初は10テナントでも問題ない。しかし100、1000と増えていくと、マイグレーションをどう全ファイルに適用するか、バックアップをどう管理するか、接続プールをどう制御するかが線形に複雑になっていく。

まとめると、SQLite本番運用の壁はこの3点だ。

  • 書き込み並列性:WALモードでも書き込みはシリアル実行、SQLITE_BUSY が頻発しやすい
  • レプリケーション:標準機能にないため外部ツール依存になる
  • マルチテナント管理:ファイル数に比例して運用コストが増大する

WALモードへの切り替えは有効な改善策だが、解決できるのは読み取りの並列性だけだ。書き込みのシリアル実行という制約は変わらない。

TursoとlibSQL ── 「どっちの話か」を先に整理する

Tursoを調べ始めると、「libSQL」と「Turso Database」という2つの名前が混在していて混乱しやすい。公式ドキュメントでも両者が並んで登場するため、何を使えばいいのかが分かりにくい。

libSQL はSQLiteのOSSフォークだ。同じファイル形式、同じAPI、完全な後方互換性を保ちながら、クラウド時代に必要な機能拡張を加えている。現時点でproduction-readyなステータスにあり、ミッションクリティカルなワークロードにも使える。

一方、Turso Database はSQLiteをRustでゼロから書き直したものだ。MVCCベースの並列書き込みや非同期I/Oを設計の中心に据えており、libSQLとは別のアプローチを取る。ただし現時点ではbetaステータスで、公式ドキュメントでも「新しいプロジェクト・エージェント・スマートデバイス向け」と位置づけられている。

本記事で扱うのは、libSQLベースのTurso SDK(@libsql/client)だ。既存のSQLiteコードからの移行コストが低く、本番運用で実績がある。「TursoとlibSQL、どっちの話か」という問いへの答えは、「本記事はlibSQLベースのSDKの話で、Turso Databaseはまだbeta」ということになる。

既存のSQLiteコードからTursoへ:差分はここだけ

Node.jsでSQLiteを使っているプロジェクトで最もよく使われているのが better-sqlite3 だろう。同期APIで使いやすく、パフォーマンスも良い。これを @libsql/client に置き換える場合、変更が必要な箇所は驚くほど少ない。

SDKのインストールは npm install @libsql/client だけでいい。ローカルファイルで動かすなら url: 'file:local.db' を指定するだけで、クラウド接続に切り替えたい場合はCLIで取得したURLとトークンを環境変数に設定すれば済む。

before/afterのコード差分を見てほしい。

// Before: better-sqlite3(同期API)
import Database from 'better-sqlite3';

const db = new Database('./local.db');

db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL
  )
`);

db.prepare('INSERT INTO users (name, email) VALUES (?, ?)').run('Alice', 'alice@example.com');

const users = db.prepare('SELECT * FROM users').all();
console.log(users);
// After: @libsql/client(Promise API)
import { createClient } from '@libsql/client';

const db = createClient({
  url: 'file:local.db', // ローカルファイルモード。クラウドはURLを差し替えるだけ
  // authToken: process.env.TURSO_AUTH_TOKEN, // クラウド接続時のみ
});

await db.execute(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL
  )
`);

await db.execute({
  sql: 'INSERT INTO users (name, email) VALUES (?, ?)',
  args: ['Alice', 'alice@example.com'],
});

const result = await db.execute('SELECT * FROM users');
console.log(result.rows);

SQL文字列はそのまま使えている。変わったのは接続部分(new Database()createClient())と、APIが同期から非同期(Promise)に変わった点だけだ。

この非同期への変化が唯一の注意点になる。better-sqlite3 の同期APIに慣れていると、await の付け忘れやトップレベルでの async 宣言漏れで詰まることがある。既存コードが同期前提で書かれている場合、呼び出し元まで波及する変更が必要になるため、そこだけは事前に把握しておきたい。

マルチテナント分離とエンベッドドレプリカ:Tursoが本領を発揮する場面

Tursoが素のSQLiteに対して明確な優位性を持つのは、マルチテナントとレプリケーションの2つの場面だ。

マルチテナントについて言えば、Tursoはテナントごとに独立したデータベースを動的に作成・接続する設計と相性がいい。素のSQLiteでテナントごとに .db ファイルを管理していた場合、マイグレーションは全ファイルにループで適用する必要があり、バックアップも個別に管理しなければならなかった。Tursoでは接続先URLをテナントごとに切り替えるだけで、コードの大部分を共通化できる。データベースの作成・削除・接続先の切り替えをAPIで制御できるため、ファイル管理の煩雑さが大幅に下がる。

もう一方のエンベッドドレプリカは、Tursoの中でも特に実用的な機能だ。仕組みはシンプルで、「読み取りはローカルファイルから、書き込みはリモートのプライマリへ」という動作をSDKレベルで実現する。アプリケーションサーバーにローカルのSQLiteファイルを置き、それをリモートDBと同期させることで、読み取りのレイテンシをネットワークラウンドトリップなしで済ませられる。素のSQLiteでは「ファイルをどうやって同期するか」を自前で実装しなければならなかった部分が、SDKの設定数行で解決する。

実装は createClientsyncUrl を渡し、任意のタイミングで client.sync() を呼ぶだけだ。

import { createClient } from '@libsql/client';

const client = createClient({
  url: 'file:replica.db',           // ローカルレプリカファイル
  syncUrl: process.env.TURSO_DATABASE_URL, // リモートプライマリ
  authToken: process.env.TURSO_AUTH_TOKEN,
});

// 起動時にリモートから最新状態を取得
await client.sync();

// 以降の読み取りはローカルから(ネットワーク不要)
const result = await client.execute('SELECT * FROM users');
console.log(result.rows);

// 書き込みはリモートプライマリへ送られ、ローカルに反映される
await client.execute({
  sql: 'INSERT INTO users (name, email) VALUES (?, ?)',
  args: ['Bob', 'bob@example.com'],
});

// 定期的に同期してローカルを最新化
await client.sync();

client.sync() の呼び出しタイミングはアプリケーション側で制御する。起動時に一度呼ぶ、あるいはリクエストのたびに呼ぶなど、ユースケースに応じて使い分けられる。このパターンは、APIサーバーが複数リージョンに分散している場合や、オフライン対応が必要なアプリケーションで特に有効だ。

「自分のプロジェクトに使えるか」を判断するための軸

Turso(libSQLベースのSDK)が有効に機能するのは、読み取りが大半を占めるワークロードだ。エンベッドドレプリカによってローカル読み取りが高速化され、書き込みはリモートプライマリに集約される設計が機能する。テナント数が多いSaaSも同様で、テナントごとにDBを分離しながら接続先をAPIで制御できる点が直接的なメリットになる。サーバーレス・エッジ環境との親和性も高く、コールドスタートの影響を受けにくいファイルベースの構造が活きる。

向いているユースケースをまとめると次のようになる。

  • 読み取り中心のワークロード(書き込みがトラフィック全体の2割程度に収まる規模)
  • テナント数が多いマルチテナントSaaS
  • サーバーレス・エッジ環境でのデプロイ
  • 素のSQLiteから移行コストを最小化したい既存プロジェクト

一方、向いていない場面も明確だ。秒間数千〜数万オーダーの大量書き込みが発生するワークロードは避けた方がいい。libSQLベースのSDKでは書き込みはシリアル実行のままであり(Turso DatabaseのMVCC並列書き込みはbetaで本番未推奨)、この制約は変わらない。複数テーブルにまたがる複雑なトランザクションが多用される設計や、すでにPostgreSQLのエコシステムに深く依存しているチームも、移行メリットが薄い。

「大量書き込み」の感覚値として参考になるのは、Rails 8のSQLite強化に関する議論の中で言及されている「秒間50,000回オーダーの書き込み」という数字だ。それ以下の規模であれば、書き込みのシリアル実行が実際のボトルネックになることは少ない。

もう一点、判断材料として持っておきたいのがlibSQLとTurso Databaseのステータスの違いだ。libSQLベースのSDKはproduction-readyで今すぐ本番に使える。Turso Database(Rust rewrite)はbetaステータスで、MVCCベースの並列書き込みなど魅力的な機能を持つが、ミッションクリティカルな用途には時期尚早だ。「Tursoを使う」という判断をする際は、どちらのTursoを使うのかを意識しておく必要がある。


@libsql/client をローカルで動かしてみると、「思っていたより変更が少ない」という感触を得られるはずだ。非同期APIへの対応さえ済ませれば、既存のSQLite資産をほぼそのまま活かしながら、レプリケーションとマルチテナント管理の選択肢が手に入る。重厚なRDBMSへの移行を検討する前の、現実的な中間の一手として試す価値はある。


株式会社ホコサキは山口県宇部を拠点に、Web制作・業務システム開発・AI活用支援・DX推進に取り組んでいます。技術選定や開発体制の相談など、お気軽にどうぞ。詳しくは お問い合わせページ からご連絡ください。

    SQLite本番運用の3つの壁とlibSQL(Turso)への移行コスト | 株式会社ホコサキ