PHP脚本响应过慢排查:显示Waiting for Cache耗时超30分钟求优化
问题根源与优化方案分析
首先可以明确:你的脚本性能瓶颈主要源于MySQL的低效查询设计,PHP层面也有可优化的点,但核心问题出在数据库的嵌套查询逻辑和缺少关键索引上。下面一步步拆解问题并给出落地优化方案:
一、为什么说是MySQL的主因?
你的代码陷入了典型的嵌套循环查询陷阱,加上缺失必要的索引,直接导致数据库查询次数呈几何级增长,进而触发"Waiting for Cache"状态——这通常是MySQL在反复执行相同/类似查询时,缓存压力过载,不得不频繁硬解析查询、等待表锁或缓存写入,最终拖垮整个响应时间。
具体来看代码里的核心问题:
多层嵌套的N+1+M查询模式
- 外层循环遍历
profile表的结果(假设返回N条记录) - 每个
profile都单独执行一次SELECT * FROM counter WHERE ...(共N次查询) - 每条
counter记录又单独执行一次SELECT ... FROM horraire WHERE ... GROUP BY ...(共M次查询,M是所有counter记录的总和)
如果N=100、M=10000,总查询次数会达到1+100+10000=10101次!这种模式完全抛弃了SQL的批量处理能力,数据库根本扛不住。
- 外层循环遍历
缺失关键联合索引
profile表的查询条件date_f_contract >= '$now' AND groupe > 0没有索引,会触发全表扫描counter表的查询条件from_interval >= '$sDateHour' AND to_interval <= '$eDateHour' AND id_profile='$pr_id'缺少联合索引,每次查询都要遍历整个counter表horraire表的查询+分组逻辑没有覆盖索引,导致频繁回表查询和排序,进一步拖慢速度
二、PHP层面的辅助问题
虽然不是核心,但这些细节也会加剧性能恶化:
- 重复实例化
Func类:代码里创建了3个Func对象($fn,$sel,$getHrName),其实完全可以复用一个,减少连接和资源开销 - 无结果缓存:同一时间范围的
horraire查询没有缓存,重复执行相同查询 - 内存管理:如果结果集过大,没有分批处理可能导致内存占用过高,但这是次要问题
三、具体优化方案
1. MySQL核心优化(必须优先做)
(1)添加必要的联合索引
执行以下SQL创建索引,直接降低查询的IO开销:
-- profile表:优化date_f_contract和groupe的组合查询 CREATE INDEX idx_profile_date_groupe ON profile(date_f_contract, groupe); -- counter表:优化按profile+时间范围的查询 CREATE INDEX idx_counter_profile_interval ON counter(id_profile, from_interval, to_interval); -- horraire表:优化按profile+时间范围的查询,同时覆盖libelle字段避免回表 CREATE INDEX idx_horraire_profile_datetime_libelle ON horraire(id_profile, date_heure_deb, date_heure_fin, libelle);
(2)重构查询逻辑,用批量JOIN替代嵌套循环
把多层嵌套的查询改成3次批量查询,然后在PHP里组装结果,彻底减少查询次数:
- 第一步:批量获取符合条件的profile
SELECT id_profile, groupe FROM profile WHERE date_f_contract >= ? AND groupe > 0 - 第二步:批量获取所有对应的counter记录
SELECT * FROM counter WHERE id_profile IN (?, ?, ...) -- 填入第一步获取的所有id_profile AND from_interval >= ? AND to_interval <= ? - 第三步:批量获取horraire的统计数据
SELECT h.id_profile, c.from_interval, c.to_interval, h.libelle AS lbl, COUNT(h.libelle) AS totalHr FROM horraire h JOIN counter c ON h.id_profile = c.id_profile AND h.date_heure_deb >= c.from_interval AND h.date_heure_fin <= c.to_interval WHERE c.id_profile IN (?, ?, ...) -- 同样填入第一步的id_profile列表 AND c.from_interval >= ? AND c.to_interval <= ? GROUP BY h.id_profile, c.from_interval, c.to_interval, h.libelle ORDER BY totalHr DESC
这样把上万次查询压缩到3次,性能会有质的飞跃。
2. PHP辅助优化
- 复用数据库连接:只实例化一个
Func对象,减少重复创建连接的开销:$db = new Func; // 所有查询都通过$db->select()执行 - 添加缓存机制:如果查询的时间范围(
sDateHour/eDateHour)不频繁变化,可以把最终的JSON结果缓存到文件或Redis,避免重复查询:$cacheKey = 'full_view_tasks_' . $sDateHour . '_' . $eDateHour; $cachedResult = file_get_contents("/tmp/{$cacheKey}.json"); if ($cachedResult) { echo $cachedResult; exit; } // 执行查询逻辑... // 保存缓存 file_put_contents("/tmp/{$cacheKey}.json", json_encode($temp, JSON_PRETTY_PRINT)); - 添加错误处理:捕获数据库查询错误,避免静默失败导致的无限等待:
$result = $db->select(...); if (!$result) { error_log(mysqli_error($db->conn)); // 假设Func类暴露了数据库连接属性conn echo json_encode(['error' => 'Database query failed']); exit; }
四、优化效果验证
优化后,先查看MySQL的慢查询日志,确认单条查询时间是否大幅降低;然后测试接口响应,正常情况下应该从30分钟降到几秒甚至毫秒级。
内容的提问来源于stack exchange,提问作者DuliNini
相关产品推荐
相关产品推荐

