You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel批量更新百万MySQL记录性能优化问题求助

高效批量更新MySQL百万级数据的Laravel方案

你的原始代码性能拉胯主要有两个核心问题:

  1. Property::with('requests')->get()会把100万条记录全部塞进PHP内存,大概率会直接触发内存溢出;
  2. 循环调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 11:11:17