mcpField
L
laravel-data-chunking-large-datasets

laravel-data-chunking-large-datasets

github.com/noartem数据与数据库1372 次安装2 周前更新
代码仓库

关于 laravel-data-chunking-large-datasets

面向 Laravel/PHP 开发者的大数据集处理指南,介绍分块与流式迭代以控制内存使用。涵盖导出、批量邮件、数据迁移和清理等任务。

主要功能

  1. 01使用 chunk 和 chunkById 分批处理
  2. 02采用 Lazy Collection 和 cursor 迭代
  3. 03内存与查询优化建议
  4. 04导出和批处理示例

适用场景

  1. 01将大型数据表导出为 CSV
  2. 02分批向订阅者发送邮件
  3. 03迁移或转换记录
  4. 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);
});

常见问题

  1. 在 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]);
            }
        });
    
  2. 未处理分块回调的返回值

    // Return false to stop chunking
    User::chunk(100, function ($users) {
        foreach ($users as $user) {
            if ($user->hasIssue()) {
                return false; // Stop processing
            }
            $user->process();
        }
    });
    
  3. 忽略数据库连接限制

    // Consider connection timeouts for long operations
    DB::connection()->getPdo()->setAttribute(PDO::ATTR_TIMEOUT, 3600);
    
    User::chunkById(100, function ($users) {
        // Long running process
    });
    

请记住:处理大型数据集时,始终要考虑内存占用、查询效率和处理时间。请合理地对数据进行分块!

评论

支持 Markdown