Laravel批量更新百万MySQL记录性能优化问题求助
高效批量更新MySQL百万级数据的Laravel方案
你的原始代码性能拉胯主要有两个核心问题:
Property::with('requests')->get()会把100万条记录全部塞进PHP内存,大概率会直接触发内存溢出;- 循环调用
save()会生成100万条独立的UPDATE语句,数据库要处理巨量请求,耗时能慢到让你怀疑人生。
下面给你几个针对不同场景的高效解决方案:
方案一:原生SQL子查询更新(性能天花板)
直接在数据库层面完成统计和更新,不需要PHP加载任何模型数据,是百万级数据更新的首选方案:
public function methodExpect() { // 更新有请求记录的properties DB::statement(" UPDATE properties p JOIN ( SELECT property_id, COUNT(*) AS request_count FROM requests GROUP BY property_id ) r ON p.id = r.property_id SET p.number_of_request = r.request_count WHERE p.number_of_request != r.request_count -- 只更新有变化的记录,减少IO开销 "); // 处理没有任何请求的properties,将number_of_request设为0 DB::statement(" UPDATE properties p LEFT JOIN requests r ON p.id = r.property_id SET p.number_of_request = 0 WHERE r.property_id IS NULL AND p.number_of_request != 0 "); }
优势:完全在数据库端执行,避免PHP内存占用,执行速度比循环更新快几个数量级,百万级数据可能几分钟就搞定。
方案二:Laravel Eloquent关联子查询更新(优雅且高效)
如果不想写原生SQL,用Laravel的joinSub方法构建关联子查询,既能保持Eloquent的语法风格,又能接近原生SQL的性能:
public function methodExpect() { // 构建请求统计的子查询 $requestCountSub = \App\Models\Request::selectRaw('property_id, COUNT(*) as request_count') ->groupBy('property_id'); // 关联子查询并批量更新 \App\Models\Property::joinSub($requestCountSub, 'r', function ($join) { $join->on('properties.id', '=', 'r.property_id'); })->update([ 'properties.number_of_request' => DB::raw('r.request_count') ]); // 批量更新无请求的记录 \App\Models\Property::whereDoesntHave('requests') ->update(['number_of_request' => 0]); }
优势:符合Laravel开发习惯,不需要写复杂的原生SQL,同时性能足够应对百万级数据。
方案三:分块处理+批量更新(适合需额外PHP逻辑的场景)
如果更新前需要在PHP层面做一些额外的业务逻辑处理,用chunk分块加载数据,避免内存溢出,再用upsert批量更新:
public function methodExpect() { // 每次从数据库拉取1000条记录处理,可根据服务器内存调整chunk大小 \App\Models\Property::with('requests')->chunk(1000, function ($properties) { $updateData = []; foreach ($properties as $property) { // 这里可以添加额外的PHP业务逻辑,比如数据校验、格式转换等 $updateData[] = [ 'id' => $property->id, 'number_of_request' => $property->requests->count() ]; } // 批量更新,每个chunk只执行1条SQL \App\Models\Property::upsert($updateData, ['id'], ['number_of_request']); }); // 单独处理无请求的记录 \App\Models\Property::whereDoesntHave('requests') ->update(['number_of_request' => 0]); }
优势:避免内存溢出,同时把SQL执行次数从100万次降到1000次左右,如果需要额外业务逻辑,这个方案灵活性更高。
关键注意事项
- 一定要给
requests表的property_id字段加索引,否则JOIN和GROUP BY的性能会差到离谱; - 尽量只更新有变化的记录(比如方案一中的
WHERE p.number_of_request != r.request_count),减少数据库IO操作; - 绝对绝对不要用
get()全量加载所有记录到内存,百万级数据下会直接让PHP内存耗尽。
内容的提问来源于stack exchange,提问作者Eranda
相关产品推荐
相关产品推荐

