本文目录导读:

- 第一阶段:定位瓶颈(最重要,占80%的工作量)
- 第二阶段:数据库侧优化(通常能解决80%的慢查询)
- 第三阶段:PHP 代码侧优化
- 第四阶段:缓存策略(降低数据库压力的“大招”)
- 第五阶段:基础设施与架构调整(终极方案)
- 给你的执行清单
PHP 慢查询优化是一个非常系统性的工程,但不要一上来就改代码,正确的顺序应该是:先定位瓶颈(在数据库还是PHP本身),再针对性优化。
以下是系统性的入手指南,按优先级排序:
第一阶段:定位瓶颈(最重要,占80%的工作量)
慢查询的根源可能在数据库(SQL慢),也可能在PHP(CPU计算慢、IO阻塞),甚至可能是在网络传输。
开启慢查询日志(数据库侧)
这是最快定位“烂SQL”的方式。
- MySQL:在
my.cnf中开启,设置阈值(例如2秒)。slow_query_log = ON long_query_time = 2 log_queries_not_using_indexes = 1
- 开启后,用
mysqldumpslow或pt-query-digest工具分析日志,找出 执行次数多 和 平均耗时高 的SQL。
使用PHP框架的调试工具(应用侧)
- Laravel:开启
APP_DEBUG=true,使用Debugbar查看页面加载时执行的每一条SQL及耗时。 - ThinkPHP:开启“Trace”和SQL日志。
- 如果没框架,建议在数据库查询入口,统一加一个 打点函数,记录每个SQL的耗时,写入日志文件。
使用 APM 工具(一键定位)
- 推荐:
SkyWalking、Pinpoint、OneAPM,这能直接告诉你整个请求链路中,是PHP函数慢,还是数据库慢,还是Redis慢。
第二阶段:数据库侧优化(通常能解决80%的慢查询)
如果确认是SQL慢,按下述步骤操作:
审视 SQL 语句(避免低级错误)
- *避免 `SELECT `**:只取需要的字段,减少数据传输量。
- 避免
LIKE '%关键词%':这会导致全表扫描,如果业务必须用,考虑引入 Elasticsearch 或搜索引擎。 - 避免函数或计算在索引列上:如
WHERE DATE(created_at) = '2023-01-01'是慢的,应写成WHERE created_at >= '2023-01-01' AND created_at < '2023-01-02'。 - 避免隐式类型转换:
WHERE phone = 13800138000(字符串字段用数字查),会导致索引失效。
分析执行计划(EXPLAIN)
对慢SQL执行 EXPLAIN SELECT ...,重点看三列:
- type:如果出现
ALL(全表扫描),必须优化。 - key:是否命中了索引(如果为 NULL,说明没走索引)。
- rows:扫描了多少行,行数越多越慢,目标是把扫描行数降下来。
建立合适索引(核心优化手段)
- 单列索引:用于高频查询的热点字段。
- 复合索引:遵循 最左前缀原则,例如查询条件是
where user_id=? and status=?,应建(user_id, status)联合索引。 - 覆盖索引:如果查询的字段都在索引里(Extra显示:Using index),查询速度会极快,无需回表。
分页优化
-
深分页问题:
LIMIT 1000000, 20很慢。 -
优化方案:延迟关联。
-- 原写法:慢 SELECT * FROM orders ORDER BY id LIMIT 1000000, 20; -- 优化后:快(通过子查询先查出主键,再关联) SELECT o.* FROM orders o INNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t ON o.id = t.id;
第三阶段:PHP 代码侧优化
如果数据库很快,但PHP响应慢,按以下顺序排查:
N+1 查询问题(最常见)
- 现象:循环查询数据库,在循环体里调用Query Builder查询,比如循环100次用户,查了100次数据库。
- 对策:使用 预加载(预载)。
- Laravel:使用
with()方法(预先加载关联关系)。 - ThinkPHP:使用
with()方法。 - 原生代码:先查用户列表,再用
IN一次性查关联数据,最后在PHP中组装。
- Laravel:使用
阻塞式 IO 调用(外部接口慢)
- 现象:PHP调用第三方API或远程接口,网络超时导致PHP进程阻塞。
- 对策:
- 设置超时时间:curl设置
CURLOPT_TIMEOUT为合理的秒数(如3秒)。 - 异步化:如果不需要立即返回结果,用消息队列(RabbitMQ/Kafka)异步处理。
- 并发请求:如果有多个不相关的HTTP请求或多个Redis查询,可以用
Swoole或curl_multi_exec并发发出,而不是串行等待。
- 设置超时时间:curl设置
循环里的复杂逻辑
- 避免在循环内做缓存读写、计算MD5、正则匹配等耗时操作。
- 提前把循环内需要的缓存数据一次性取出来(如批量获取用户信息,而不是每条取一次)。
内存溢出与GC
- 大数据量循环处理时,排查是否大量使用静态数组导致内存暴涨,进而影响GC性能。
第四阶段:缓存策略(降低数据库压力的“大招”)
很多慢查询是因为QPS太高,数据库“忙不过来”。
- Redis 缓存:把热点数据(如商品详情、用户信息)缓存到Redis。
- 更新策略:Cache Aside Pattern(先更新数据库,再删缓存)。
- 本地缓存(APCu / Memcached):应对进程内高频读取的基础数据。
第五阶段:基础设施与架构调整(终极方案)
如果上述都做了还是慢,可能是硬件或架构问题:
- 读写分离:主库负责写,从库负责读,分散压力。
- 分库分表:单表数据量超千万行时,索引再优化也慢,需要水平拆分。
- 硬件升级:更换SSD硬盘(NVMe)、增加内存(让MySQL的InnoDB Buffer Pool足够大,热数据全在内存)——这是最直接有效但成本最高的手段。
给你的执行清单
如果现在就要开始干,请按这个顺序做:
- 开启慢查询日志,抓出最慢的20条SQL。
- 用
EXPLAIN分析,查看type和rows,为慢SQL 建立/调整索引。 - 检查应用代码,搜索循环中的 SQL 查询,改成
IN批量查询或使用框架的预加载。 - 为热点数据加 Redis 缓存(比如首页数据、分类数据)。
- 重复测试,对比优化前后的
Query Time耗时。
提示:优化时,不要靠猜,先看监控数据,如果数据库CPU占用率很低,但PHP CPU飙高,那问题在代码;如果数据库CPU 100%,那基本就是SQL或索引的问题。