Laravel处理200万+行大表时,按月/按周统计出现内存超限、加载过慢如何优化
性能问题根源
你当前代码查询慢、触发内存限制的核心原因有4个:
- 近3个月的统计没有在数据库层完成聚合计算,直接把全量符合条件的POI记录全部拉取到PHP内存中,实例化为Eloquent模型后再用集合方法分组计数。当POI表数据量超过10万行时,仅模型实例的内存占用就会直接超过PHP的内存限制
- 原代码用
whereYear/whereMonth对created_at字段做函数处理,MySQL无法命中索引,必须全表扫描判断 - 存在冗余无意义操作:拉取全量数据后调用了
sortBy排序,但后续逻辑根本没有用到排序结果,白白消耗CPU和内存资源 - 存在拼写bug:空数据分支里把
pois写成了pots,会触发未定义数组键的运行异常
优化步骤
1. 把聚合计算全部下推到数据库层
所有计数、分组逻辑全部交给MySQL执行,PHP侧只接收最终的统计结果,单查询的内存占用可以从全量行级别的MB级开销降到KB级
当月POI计数优化
替换掉会导致索引失效的whereYear/whereMonth写法,用时间范围的闭区间判断:
// 取当月1号的零点时间作为起始值 $currentMonthStart = Carbon::now()->startOfMonth(); $data['pois']['total'] = PoiLocation::where('created_at', '>=', $currentMonthStart) ->where('created_at', '<', Carbon::now()) ->count();
对字段加函数判断的查询写法会让MySQL放弃普通索引的使用,必须逐行扫描判断,数据量越大速度下降越明显。
近3个月ISO周维度统计优化
直接在SQL层用MySQL内置的YEARWEEK函数(参数传3时完全匹配ISO周计算规则)完成分组计数,完全不需要拉取全量数据到PHP:
// 近3个月的时间边界 $statStart = Carbon::now()->subMonths(3); // 数据库层完成分组、计数,只返回每个周对应的统计值 $data['pois']['weeks'] = PoiLocation::query() ->where('created_at', '>=', $statStart) ->selectRaw('YEARWEEK(created_at, 3) as iso_week, COUNT(*) as week_count') ->groupBy('iso_week') ->pluck('week_count', 'iso_week') ->toArray();
注意:原代码用Carbon的isoWeek()取的是不带年份的周数,跨年度的3个月统计里会把不同年份的同周数数据合并到同一个key里,导致统计错配。MySQL的YEARWEEK(时间,3)生成的是带年份的周标识,更符合长期统计的逻辑
2. 数据库索引配套优化
给created_at字段增加单列索引,保障上述范围查询、分组聚合的执行效率:
ALTER TABLE `poi_locations` ADD INDEX `idx_created_at` (`created_at`);
索引生效后,上述两个查询的扫描范围会从全表缩小到对应时间区间的索引范围,通常查询速度可以提升100倍以上。
3. 修正原代码的逻辑bug
原代码计算最高周数的分支存在拼写错误,补上修正后的兜底逻辑:
if (!empty($data['pois']['weeks'])) { $data['pois']['high'] = max($data['pois']['weeks']); } else { $data['pois']['high'] = 1; }
可选进阶优化
如果后续统计周期继续拉长,可以增加一张按周预聚合的统计表,提前把每周的POI计数结果存在表里,每次新增数据时同步更新预聚合值,统计查询时直接读取预聚合表的结果,性能可以再提升一个台阶。
内容的提问来源于stack exchange,提问作者Martin M
相关产品推荐
相关产品推荐

