laravel-data-chunking-large-datasets について
Laravel/PHPで大規模なデータベース結果を扱う開発者向けのスキルです。チャンク処理やストリーム型の反復でメモリ使用量を抑え、エクスポートや移行などの例を示します。
主な機能
- 01chunkとchunkByIdによる処理
- 02Lazy Collectionとcursorによる反復
- 03メモリとクエリの最適化ガイド
- 04エクスポートとバッチ処理の例
活用シーン
- 01大規模テーブルのCSV出力
- 02購読者へのメール一括送信
- 03レコードの移行や変換
- 04古いデータベースレコードの削除
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ noartem/ skills --skill laravel-data-chunking-large-datasetsインストール後、Claude Code で/laravel-data-chunking-large-datasetsを入力して読み込まれたか確認します。
詳細
大規模データセットのチャンク処理
大規模データセットを扱いやすいチャンクに分割して効率的に処理し、メモリ消費量を抑えてパフォーマンスを向上させます。
問題:メモリの枯渇
// BAD: Loading all records into memory
$users = User::all(); // Could be millions of records!
foreach ($users as $user) {
$user->sendNewsletter();
}
// BAD: Even with select, still loads everything
$emails = User::pluck('email'); // Array of millions of emails
foreach ($emails as $email) {
Mail::to($email)->send(new Newsletter());
}
解決策:チャンク処理の方法
1. chunk() を使った基本的なチャンク処理
// Process 100 records at a time
User::chunk(100, function ($users) {
foreach ($users as $user) {
$user->calculateStatistics();
$user->save();
}
});
// With conditions
User::where('active', true)
->chunk(200, function ($users) {
foreach ($users as $user) {
ProcessUserJob::dispatch($user);
}
});
2. より安全な更新のための ID によるチャンク処理
// Prevents issues when modifying records during iteration
User::where('newsletter_sent', false)
->chunkById(100, function ($users) {
foreach ($users as $user) {
$user->update(['newsletter_sent' => true]);
Mail::to($user)->send(new Newsletter());
}
});
// With custom column
Payment::where('processed', false)
->chunkById(100, function ($payments) {
foreach ($payments as $payment) {
$payment->process();
}
}, 'payment_id'); // Custom ID column
3. メモリ効率を高める遅延コレクション
// Uses PHP generators, minimal memory footprint
User::where('created_at', '>=', now()->subDays(30))
->lazy()
->each(function ($user) {
$user->recalculateScore();
});
// With chunking size control
User::lazy(100)->each(function ($user) {
ProcessRecentUser::dispatch($user);
});
// Filter and map with lazy collections
$results = User::lazy()
->filter(fn($user) => $user->hasActiveSubscription())
->map(fn($user) => [
'id' => $user->id,
'revenue' => $user->calculateRevenue(),
])
->take(1000);
4. 前方向のみの反復処理に使うカーソル
// Most memory-efficient for simple forward iteration
foreach (User::where('active', true)->cursor() as $user) {
$user->updateLastSeen();
}
// With lazy() for additional collection methods
User::where('verified', true)
->cursor()
->filter(fn($user) => $user->hasCompletedProfile())
->each(fn($user) => SendWelcomeEmail::dispatch($user));
実際の使用例
大規模な CSV のエクスポート
class ExportUsersCommand extends Command
{
public function handle()
{
$file = fopen('users.csv', 'w');
// Write headers
fputcsv($file, ['ID', 'Name', 'Email', 'Created At']);
// Process in chunks to avoid memory issues
User::select('id', 'name', 'email', 'created_at')
->chunkById(500, function ($users) use ($file) {
foreach ($users as $user) {
fputcsv($file, [
$user->id,
$user->name,
$user->email,
$user->created_at->toDateTimeString(),
]);
}
// Optional: Show progress
$this->info("Processed up to ID: {$users->last()->id}");
});
fclose($file);
$this->info('Export completed!');
}
}
メールの一括配信キャンペーン
class SendCampaignJob implements ShouldQueue
{
public function handle()
{
$campaign = Campaign::find($this->campaignId);
// Process subscribers in chunks
$campaign->subscribers()
->where('unsubscribed', false)
->chunkById(50, function ($subscribers) use ($campaign) {
foreach ($subscribers as $subscriber) {
SendCampaignEmail::dispatch($campaign, $subscriber)
->onQueue('emails')
->delay(now()->addSeconds(rand(1, 10)));
}
// Prevent rate limiting
sleep(2);
});
}
}
データの移行/変換
class MigrateUserData extends Command
{
public function handle()
{
$bar = $this->output->createProgressBar(User::count());
User::with(['profile', 'settings'])
->chunkById(100, function ($users) use ($bar) {
DB::transaction(function () use ($users, $bar) {
foreach ($users as $user) {
// Complex transformation
$newData = $this->transformUserData($user);
NewUserModel::create($newData);
$bar->advance();
}
});
});
$bar->finish();
$this->newLine();
$this->info('Migration completed!');
}
}
古いレコードのクリーンアップ
class CleanupOldLogs extends Command
{
public function handle()
{
$deletedCount = 0;
ActivityLog::where('created_at', '<', now()->subMonths(6))
->chunkById(1000, function ($logs) use (&$deletedCount) {
$ids = $logs->pluck('id')->toArray();
// Batch delete for efficiency
ActivityLog::whereIn('id', $ids)->delete();
$deletedCount += count($ids);
$this->info("Deleted {$deletedCount} records so far...");
// Give database a breather
usleep(100000); // 100ms
});
$this->info("Total deleted: {$deletedCount}");
}
}
適切な方法の選択
| 方法 | 用途 | メモリ使用量 | 備考 |
|---|---|---|---|
chunk() | 一般的な処理 | 中程度 | フィルター対象の列を変更すると、スキップや重複が発生することがあります |
chunkById() | 反復処理中の更新 | 中程度 | 変更を伴う処理により安全です |
lazy() | 大規模な結果の処理 | 少ない | LazyCollection を返します |
cursor() | シンプルな前方向の反復処理 | 最小 | Generator を返します |
each() | シンプルな操作 | 多い(すべて読み込む) | 大規模なデータセットでは避けてください |
パフォーマンスを最適化するヒント
1. 必要な列だけを選択する
User::select('id', 'email', 'name')
->chunkById(100, function ($users) {
// Process with minimal data
});
2. インデックスを使用する
// Ensure indexed columns in where clauses
User::where('status', 'active') // status should be indexed
->where('created_at', '>', $date) // created_at should be indexed
->chunkById(200, function ($users) {
// Process efficiently
});
3. 適切な場合は Eloquent のイベントを無効にする
User::withoutEvents(function () {
User::chunkById(500, function ($users) {
foreach ($users as $user) {
$user->update(['processed' => true]);
}
});
});
4. 一括更新には生のクエリを使用する
// Instead of updating each record
User::chunkById(100, function ($users) {
$ids = $users->pluck('id')->toArray();
// Bulk update with raw query
DB::table('users')
->whereIn('id', $ids)
->update([
'last_processed_at' => now(),
'processing_count' => DB::raw('processing_count + 1'),
]);
});
5. 大規模な処理をキューに入れる
class ProcessLargeDataset extends Command
{
public function handle()
{
User::chunkById(100, function ($users) {
ProcessUserBatch::dispatch($users->pluck('id'))
->onQueue('heavy-processing');
});
}
}
class ProcessUserBatch implements ShouldQueue
{
public function __construct(
public Collection $userIds
) {}
public function handle()
{
User::whereIn('id', $this->userIds)
->get()
->each(fn($user) => $user->process());
}
}
チャンク処理のテスト
test('processes all active users in chunks', function () {
// Create test data
User::factory()->count(150)->create(['active' => true]);
User::factory()->count(50)->create(['active' => false]);
$processed = [];
User::where('active', true)
->chunkById(50, function ($users) use (&$processed) {
foreach ($users as $user) {
$processed[] = $user->id;
}
});
expect($processed)->toHaveCount(150);
expect(count(array_unique($processed)))->toBe(150);
});
test('handles empty datasets gracefully', function () {
$callCount = 0;
User::where('id', '<', 0) // No results
->chunk(100, function ($users) use (&$callCount) {
$callCount++;
});
expect($callCount)->toBe(0);
});
よくある落とし穴
-
chunk() の実行中にフィルター対象の列を変更する
// WRONG: May skip records User::where('processed', false) ->chunk(100, function ($users) { foreach ($users as $user) { $user->update(['processed' => true]); // Changes the WHERE condition! } }); // CORRECT: Use chunkById() User::where('processed', false) ->chunkById(100, function ($users) { foreach ($users as $user) { $user->update(['processed' => true]); } }); -
チャンクのコールバックの戻り値を処理しない
// Return false to stop chunking User::chunk(100, function ($users) { foreach ($users as $user) { if ($user->hasIssue()) { return false; // Stop processing } $user->process(); } }); -
データベース接続の上限を無視する
// Consider connection timeouts for long operations DB::connection()->getPdo()->setAttribute(PDO::ATTR_TIMEOUT, 3600); User::chunkById(100, function ($users) { // Long running process });
大規模なデータセットを扱うときは、メモリ使用量、クエリ効率、処理時間を常に考慮してください。データを適切にチャンク分割しましょう!
コメント
Markdown 対応