2025年12月13日
この記事はQiita「Laravel Advent Calendar 2025」参加記事です。
「SQLiteでも本番運用に耐えられるのでは」的な話を聞いたり聞かなかったりするので、パフォーマンスを自分なりに検証してみる。
もし、結構いけそうなら個人開発レベルの本番運用に採用したい。
検証方法
なんちゃって検証なので、ローカルで検証する。
動作環境
- Kubuntu OS
- Laravel12
- PHP: 8.4
- SQLite: 3.46.1
- MySQL: 8.4.7
- Laravel Octane(FrankenPHP)
- 理由: PHP-FPMだと同時書き込み量的にPHP-FPMが自体がボトルネックになりうるので
上記の環境をDockerで構築して、MySQLの場合とSQLiteの場合で比較する。
- 理由: PHP-FPMだと同時書き込み量的にPHP-FPMが自体がボトルネックになりうるので
また、他情報として
- 負荷テストにはKubuntuからk6を使う
- ストレージにはwrite/read共に1500Mib以上のSSDを利用している
SQLiteの設定(PRAGMA)
SQLiteには色々設定(PRAGMA)があるっぽい。
以下の2つだけ明示的に設定を指定し、あとはLaravelにお任せしている。
journal_mode: 以下2通りそれぞれ検証DELETEWAL
synchronous:NORMAL
MySQLの設定
こちらは速度に関係ありそうな設定はなにも弄らずMySQLイメージそのままの初期設定にしている。
DBの状態
事前にシーダーで10GBくらいデータを入れておく。
毎回初期化するのは面倒なので、データは1回いれたらそのまま。
検証順で若干のデータ量の揺れはあるけど、微差なはずなのでそこは妥協。
$ ls database.sqlite -l
-rw-rw-r-- 1 tamakoma tamakoma 10694750208 12月 13 20:07 database.sqlite
MySQLの方、ちょっと入れすぎた
mysql> SELECT
-> table_schema AS 'Database',
-> ROUND(SUM(data_length + index_length) / 1024 / 1024 / 1024, 2) AS 'Size (GB)'
-> FROM information_schema.tables
-> WHERE table_schema = 'laravel'
-> GROUP BY table_schema;
+----------+-----------+
| Database | Size (GB) |
+----------+-----------+
| laravel | 12.23 |
+----------+-----------+
articlesのレコード数は大体300万件くらいだった。
マイグレーションファイル
以下のマイグレーションファイルでusersとarticlesテーブルを作り利用する。
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
public function up(): void
{
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->text('body');
$table->timestamps();
});
}
DBの操作コード
以下のルートへアクセスすることで、適当なデータを作成 or 読み込みするようになっている。
記事のtitleやbodyは'body' => Str::repeat("あ", 1000),で固定値を生成。
Route::post('articles', function () {
$article = Article::factory()->create();
return response()->json($article->load('user'), 201);
});
Route::get('articles', function () {
$articles = Article::orderBy('id', 'desc')
->limit(20)
->get();
return response()->json($articles);
});
検証する
読み取りのテスト
以下のk6コードでテストする。
import http from 'k6/http';
import { sleep, check } from 'k6';
export const options = {
vus: 50,
duration: '30s',
};
export default function () {
const res = http.get('http://localhost/api/articles');
check(res, { "status is 200": (res) => res.status === 200 });
sleep(0.1);
}
やってることは
- 30秒間
- 50人同時に
- 0.1秒ごとに読み取りリクエストを送る
という感じ。
結果は以下
| 項目 | SQLite(delete) | SQLite(wal) | MySQL |
|---|---|---|---|
| 平均レスポンス | 4.5ms | 4.42ms | 3.49ms |
| 中央値レスポンス | 4.16ms | 4.05ms | 3.19ms |
| 最大値レスポンス | 45.74 | 75.33 | 33.58ms |
| レスポンス成功率 | 100.00% | 100.00% | 100.00% |
| 総リクエスト | 14299 | 14312 | 14450 |
書き込みのテスト
以下のk6コードでテストする。
import http from 'k6/http';
import { sleep, check } from 'k6';
export const options = {
vus: 10,
duration: '30s',
};
export default function () {
const res = http.post('http://localhost/api/articles');
check(res, { "status is 201": (res) => res.status === 201 });
sleep(0.1);
}
やってることは
- 30秒間
- 10人同時に
- 0.1秒ごとに書き込みリクエストが来る
という感じ。
結果は以下
| 項目 | SQLite(delete) | SQLite(wal) | MySQL |
|---|---|---|---|
| 平均レスポンス | 11.62ms | 5.27ms | 14.82ms |
| 中央値レスポンス | 8.52ms | 2.81ms | 11.98ms |
| 最大値レスポンス | 840.39ms | 440.04ms | 322.94ms |
| レスポンス成功率 | 100.00% | 100.00% | 100.00% |
| 総リクエスト | 2677 | 2863 | 2603 |
MySQLよりSQLiteの方がこの値を見る分にはパフォーマンスが良いのにビビっている。
30同時接続の書き込みのテスト
思ったよりSQLiteがいけるので、もう少し負荷を上げる。
Articleの作成、編集、削除、トランザクションを詰め込んだコードを以下のように書き
Route::post('articles2', function () {
DB::transaction(function () {
$article = Article::factory()->create();
$article->title = "いいいいいいい";
$article->save();
$article->delete();
});
return response()->json('ok');
});
以下のように同時接続数を3倍の30にしたものでやってみる。
import http from 'k6/http';
import { sleep, check } from 'k6';
export const options = {
vus: 30,
duration: '30s',
};
export default function () {
const res = http.post('http://localhost/api/articles2');
check(res, { "status is 200": (res) => res.status === 200 });
sleep(0.1);
}
結果は以下
| 項目 | SQLite(delete) | SQLite(wal) | MySQL |
|---|---|---|---|
| 平均レスポンス | 36.52ms | 4.5ms | 12ms |
| 中央値レスポンス | 6.63ms | 2.31ms | 9.73ms |
| 最大値レスポンス | 4.44s | 480.4ms | 506.89ms |
| レスポンス成功率 | 100.00% | 100.00% | 100.00% |
| 総リクエスト | 6579 | 8583 | 7989 |
SQLite(delete)は明らかにここらへんで処理能力が追いつけなくなっている。
50同時接続の書き込みテスト
30でもまだ行けそうなのでさっきのテストコードの同時書き込みを50にしてみる。
結果は以下
| 項目 | SQLite(delete) | SQLite(wal) | MySQL |
|---|---|---|---|
| 平均レスポンス | 166.29ms | 4.72ms | 17.12ms |
| 中央値レスポンス | 52.66ms | 2.35ms | 14.16ms |
| 最大値レスポンス | 4.37s | 597ms | 600.29ms |
| レスポンス成功率 | 100.00% | 100.00% | 100.00% |
| 総リクエスト | 5644 | 14278 | 12762 |
SQLite(delete)はもう無理だけど、SQLite(wal)とMySQLはまだ戦えそう。
100同時接続の書き込みテスト
50でもまだいけそうなので、同時接続を100にしてみる。
結果は以下
| 項目 | SQLite(delete) | SQLite(wal) | MySQL |
|---|---|---|---|
| 平均レスポンス | 343.3ms | 16.76ms | 19.36ms |
| 中央値レスポンス | 249.58ms | 4.61ms | 14.14ms |
| 最大値レスポンス | 3.85s | 1.73s | 553.03ms |
| レスポンス成功率 | 100.00% | 100.00% | 100.00% |
| 総リクエスト | 6809 | 25636 | 25051 |
私の環境だとだいたいここらへんが限界そう。
まとめ
私の環境では
SQLite(wal) >= MySQL > SQLite(delete)
という順でパフォーマンスが良かった。
なぜSQLiteの方がパフォーマンスが出たかなんだけど、MySQLの方は別コンテナで動かしているのでその分コンテナ間の通信に時間がかかっちゃってたんだと思う。ただ、そのコンテナ間通信が要らないというのもSQLiteの強みなので、しっかりその強みが見れたのかなと。
30秒間で25636リクエスト = 1秒間に850リクエストくらいあの書き込みを処理できるってことなので、思ったよりSQLiteすごい。
SQLiteとサーバー型のDBの使い分け
じゃあ全部SQLiteで良いのかっていうとそういうわけでもない。
実はSQLiteの公式サイトに使い分けに関しては載ってて、以下の場合はSQLiteよりMySQLとかの方が良い
- SQLiteのファイルとアプリケーションサーバーが同じ物理デバイス上にない
- 同時書き込みが多い
- 281テラバイト以上のデータを扱いたい
今回2番の同時書き込みに関してはかなり余裕があることがわかったので、サーバーが1台で足りる状態なら私は今後SQLiteを使うかなと思う。
ただ、やっぱりサーバーをスケーリングしてSQLiteをネットワークファイルシステムに置いて〜みたいになってくるとSQLiteは仕組み上不整合が起きるみたいなので、その場合はMySQLに移ったりする必要があるのかなと。
以上です。