获取各区域含当前活动城市数量时脚本崩溃求助
问题诊断与解决方案
首先,你的脚本崩溃主要源于两个核心问题:逻辑错误导致的无效计算,叠加N+1查询引发的性能爆炸,最终触发了"Request too heavy"的告警。
1. 先修正核心逻辑错误
你原本的需求是统计「每个区域中存在当前活动的城市数量」,但现有代码的第二个循环里,每次遍历城市都会覆盖$regionCitiesNumber[$index]的值,最后这个变量只会保留该区域最后一个城市的活动数量,完全不符合你的需求。正确逻辑应该是:遍历每个城市,判断它是否有当前活动(活动数>0),如果有就给区域计数+1。
2. 解决性能爆炸问题(N+1查询)
你的代码在第一个循环加载所有区域的城市后,第二个循环里每个城市调用getCurrentEvents()时,Doctrine会懒加载每个城市的events关联,再用matching()过滤——这意味着每个城市都会发起至少一次额外的数据库查询。如果区域和城市数量较多,几百上千次查询会瞬间拉满请求负载,导致脚本崩溃。
优化方案一:预加载符合条件的关联数据,减少查询次数
可以在获取城市的时候,直接通过Doctrine的QueryBuilder预加载仅当前活动的events,避免后续的懒加载查询。修改CityRepository中的方法:
// 在CityRepository中 public function getCitiesByRegionWithCurrentEvents(Region $region): array { $currentDate = new DateTime('00:00:00'); return $this->createQueryBuilder('c') ->select('c, e') // 预加载符合条件的events ->leftJoin('c.events', 'e', 'WITH', 'e.endDate >= :currentDate') ->where('c.region = :region') ->setParameters([ 'region' => $region, 'currentDate' => $currentDate, ]) ->getQuery() ->getResult(); }
然后修改业务代码:
$regionCities = []; foreach ($regions as $region) { // 使用新的预加载方法 $regionCities[$region->getId()] = $cityRepository->getCitiesByRegionWithCurrentEvents($region); } $regionCitiesNumber = []; foreach ($regionCities as $regionId => $cities) { $count = 0; foreach ($cities as $city) { // 直接用已预加载的集合判断,不会触发额外查询 if (!$city->getCurrentEvents()->isEmpty()) { $count++; } } $regionCitiesNumber[$regionId] = $count; }
优化方案二:直接在数据库层面完成统计(最优解)
比起加载所有实体再计算,直接让数据库统计数量效率高得多,完全避免了加载大量实体带来的内存和性能开销。可以在RegionRepository中写一个专门的统计方法:
// 在RegionRepository中 public function getRegionActiveCityCount(): array { $currentDate = new DateTime('00:00:00'); $result = $this->createQueryBuilder('r') ->select('r.id as regionId, COUNT(DISTINCT c.id) as activeCityCount') ->leftJoin('r.cities', 'c') ->leftJoin('c.events', 'e', 'WITH', 'e.endDate >= :currentDate') ->where('e.id IS NOT NULL') // 只统计有当前活动的城市 ->groupBy('r.id') ->setParameter('currentDate', $currentDate) ->getQuery() ->getResult(); // 转换成键值对格式方便使用 $counts = []; foreach ($result as $item) { $counts[$item['regionId']] = $item['activeCityCount']; } // 给没有活动城市的区域补0 foreach ($this->findAll() as $region) { if (!isset($counts[$region->getId()])) { $counts[$region->getId()] = 0; } } return $counts; }
使用时直接调用即可,一行代码搞定:
$regionCitiesNumber = $regionRepository->getRegionActiveCityCount();
这个方案只需要一次数据库查询,性能最优,完全不会出现"Request too heavy"的问题。
内容的提问来源于stack exchange,提问作者yinyang
相关产品推荐
相关产品推荐

