はじめに
実務でLaravelでチャットシステムを開発した際に、大量のデータをデータベースに保存する処理があり、その際に行ったデータベースの最適化についての備忘録を残しておきます。今回はデータベース処理の最適化で使われるうちの、「チャンク処理」と「バルク処理」について説明していきます。間違っているところなどありましたら、易しくご教授いただけると大変ありがたいです。
Eloquentモデル、チャンク処理、バルク処理の違い
それぞれの違いについて、詳細に解説をしますが、始めに主な違いについて表にまとめておきます。下の表を見てわかるように、この中ですとバルク処理が一番速度も速く低いメモリ量で処理をすることが可能です。
▼表
(例)10,000件のユーザーデータを登録する場合
方法 | クエリ数 | メモリ使用量 | 速度 | モデルイベント |
---|---|---|---|---|
Eloquentモデル | 10,000 | 高い | 非常に遅い | 動作する |
チャンク処理 | 依然として多い | 制御可能 | 中程度 | 動作する |
バルク操作 | 1〜10程度 | 低い | 非常に高速 | 動作しない |
Eloquentモデルでの処理
まず初めにLaravelでDB操作でよく使用されるEloquentモデルでの処理を確認してみます。1000件のユーザーデータをDBに挿入し、その際のメモリの消費量と速度を実際に確認します。
今回はデバッグでClockWorkを使用しています。
1. コードの準備
まず、ユーザーデータを1000件作成しデータベースに保存するコードを記述します。
▼特徴
- 各レコードごとにクエリが実行される(1000クエリ)
- 大量処理の場合は、処理が遅い
- トランザクション、タイムスタンプが自動的に処理される
// ユーザーデータを1000個生成する
$users = [];
for ($i = 1; $i <= 1000; $i++) {
$username = 'user' . $i . '_' . substr(md5(mt_rand()), 0, 5);
$email = 'user' . $i . '_' . substr(md5(mt_rand()), 0, 6) . '@example.com';
$users[] = [
'username' => $username,
'email' => $email,
];
};
clock()->event('eloquent_operation')->begin('Eloquent全件取得');
// 1件ずつ保存
foreach ($users as $user) {
ChatUser::create($user);
}
clock()->event('eloquent_operation')->end();
2. clockWorkで確認
▼処理速度・メモリ使用量
- 処理速度: 4562ms
- メモリ使用量: 34MB
▼データベース処理
- クエリ数: 1002件
- データベース処理時間: 3457ms
チャンク処理
続いて、チャンク処理を確認してみます。チャンク処理とは大量のデータを小さなグループに分割して処理する方法のことを指します。
1. コードの準備
先程と同じようにユーザーデータを1000件作成しデータベースに保存するコードを記述します。
▼特徴
- メモリ使用量を制御できる
- 大量データでもタイムアウトしにくい
- モデルの機能は維持される
- 各レコード単位でのクエリは依然として発生する
// チャンク処理
clock()->event('chunk_operation')->begin('チャンク処理');
// 全データを一度に生成せず、チャンクごとに処理
for ($chunk = 0; $chunk < 50; $chunk++) { // 20件×50=1000件
// このチャンクのデータだけを生成
$chunk_users = [];
for ($i = 1; $i <= 20; $i++) {
$index = $chunk * 20 + $i;
$chunk_users[] = [
'username' => 'user' . $index . '_' . substr(md5(mt_rand()), 0, 5),
'email' => 'user' . $index . '_' . substr(md5(mt_rand()), 0, 6) . '@example.com',
];
}
foreach ($chunk_users as $user) {
ChatUser::create($user);
}
// チャンク処理後にメモリを解放
unset($chunk_users);
}
clock()->event('chunk_operation')->end();
2. clockWorkで確認
▼処理速度・メモリ使用量
- 処理速度: 3059ms
- メモリ使用量: 32MB
▼データベース処理
- クエリ数: 1002件
- データベース処理時間: 1911ms
バルク処理
続いて、バルク処理を確認してみます。バルク処理とは、個別の操作をまとめて一括で行う方法のことを指します。
1. コードの準備
先程と同じようにユーザーデータを1000件作成しデータベースに保存するコードを記述します。
▼特徴
- 最も高速(1回のクエリで多数のレコードを処理)
- クエリビルダを使用
- メモリ効率が良い
- モデルイベントが発火しない
// ユーザーデータを1000個生成する
$users = [];
for ($i = 1; $i <= 1000; $i++) {
$username = 'user' . $i . '_' . substr(md5(mt_rand()), 0, 5);
$email = 'user' . $i . '_' . substr(md5(mt_rand()), 0, 6) . '@example.com';
$users[] = [
'username' => $username,
'email' => $email,
];
};
// Clockworkでイベント計測
clock()->event('bulk_insert')->begin('バルク挿入処理');
// バルク挿入実行
DB::table('users')->insert($users);
clock()->event('bulk_insert')->end();
2. clockWorkで確認
▼処理速度・メモリ使用量
- 処理速度: 287ms
- メモリ使用量: 22MB
▼データベース処理
- クエリ数: 3件
- データベース処理時間: 21ms
結果まとめ
やはり一目瞭然でわかるように、今回はバルク処理がずば抜けて高速で、メモリ消費量も抑えられることがわかりました。今回は1000件のみのデータベース操作でしたが、10000件、それ以上にデータ数が増えていくと、違いがもっとわかるかと思います。
方法 | クエリ数 | メモリ使用量 | 速度 |
---|---|---|---|
Eloquentモデル | 1002件 | 34MB | 4562ms |
チャンク処理 | 1002件 | 32MB | 3059ms |
バルク操作 | 3件 | 22MB | 287ms |
いつどの処理を使うべきか
紹介した3つの方法で、どの場面でその処理が適しているのでしょうか?いろいろ意見はあると思いますが、何点か挙げていきます。
1. Eloquentモデル
(例)ユーザー登録時(イベント発火、バリデーションが必要)
- 少量のデータ処理(数百件程度)
- モデルイベント、リレーションなどが重要
- 開発の読みやすさなどを優先する
2. チャンク処理
(例)数万件のユーザーにポイント付与
- 大量データの読み取りと処理(数千〜数万件)
- メモリ使用量を制限する必要がある
- モデル機能を維持しながら大量データを扱う
3. バルク処理
(例)数十万件の商品データ一括保存
- 非常に大量のデータ操作(数万〜数百万件)
- 最高速のパフォーマンスが必要
- シンプルな一括操作(挿入/更新/削除)
おわりに
データベースの最適化はまだまだ多くの手法があると思うので、引き続き学んでいきたいと思います。最後までご覧いただきありがとうございました!