Laravel查询15万+MySQL记录响应慢报500错如何优化
当前代码属于典型的N+1查询反模式:先执行1次查询拉取全部900条Population数据,再循环对每条Population单独发起1次COUNT查询,累计产生901次数据库请求,大量连接开销、网络IO是性能差的首要原因。
另外你目前创建的是4个字段的单列索引,MySQL单表查询一次最多只能选择一个最优的单列索引使用,其余过滤条件需要回表逐行判断,无法通过索引一次性完成所有过滤逻辑,所以加了索引也没有明显性能提升。
合并零散查询为单条聚合查询,消除N+1问题
把900次循环COUNT的逻辑替换为一次分组聚合查询,直接在数据库侧完成所有population_id的计数统计,总查询次数从901次降到2次,Laravel实现代码如下:// 一次性统计所有符合条件的关联记录数,返回 [population_id => 计数] 的映射数组 $validRecipientCount = Recipient::selectRaw('population_id, COUNT(*) as total') ->whereIn('employment_condition', [0,1,3]) ->where('has_car', '!=', 0) ->where('has_land', '!=', 0) ->groupBy('population_id') ->pluck('total', 'population_id') ->toArray(); // 拉取Population全量数据,映射统计结果 $pops = Population::all()->map(function ($pop) use ($validRecipientCount) { $pop['count'] = $validRecipientCount[$pop->id] ?? 0; return $pop; });如果已经在模型中定义了Population和Recipient的一对多关联,也可以直接用框架自带的
withCount方法实现,底层逻辑和上述代码一致:$pops = Population::withCount([ 'recipients' => function ($query) { $query->whereIn('employment_condition', [0,1,3]) ->where('has_car', '!=', 0) ->where('has_land', '!=', 0); } ])->get();替换单列索引为联合覆盖索引,消除回表开销
针对这个固定查询场景,删除之前建的4个单列索引,创建如下联合覆盖索引:ALTER TABLE recipients ADD INDEX idx_pop_filter (population_id, employment_condition, has_car, has_land);该索引按照等值匹配优先的顺序排列字段,查询时可以直接在索引树上完成过滤、分组、计数全流程,不需要回表读取行数据,查询效率会有数量级提升。
可选进阶优化(适合高频查询场景)
如果该统计接口调用频率很高,可以在Population表新增valid_recipient_count字段,在Recipient数据新增、修改、删除时,同步更新对应关联Population的计数值,查询时直接读取该字段即可,不需要每次实时聚合统计,响应时间可以降到毫秒级。
建立联合索引时,不要把带
!=的不等值条件放在索引最左侧,不等值条件区分度低,会大幅降低索引过滤效率,上述索引顺序已经将等值匹配的population_id、employment_condition放在靠前位置,能最大化索引利用率。
内容的提问来源于stack exchange,提问作者Mohammed Ali

