
你有没有遇到过这种情况?PHP写的网站刚上线时跑得飞快,可用户量一上来,页面加载就像蜗牛爬——商品列表页转半天出不来,下单时提示“系统繁忙”,后台数据统计页面甚至直接超时。我去年帮朋友的电商网站排查过类似问题,当时他们日均订单才300多,数据库就频繁卡死,后来发现是几条慢查询在拖后腿,优化后页面响应从3秒压到了200毫秒。今天咱们就从“怎么找到问题”开始,一步步把MySQL性能诊断的流程讲透,你跟着做,就算是小白也能定位大部分性能瓶颈。
慢查询日志:性能问题的“黑匣子”
想优化数据库,得先知道“谁在搞事”。MySQL的慢查询日志就像飞机的黑匣子,能记录所有执行时间超过设定阈值的SQL语句。我通常会 把这个“阈值”设为1秒(long_query_time=1),因为对用户来说,1秒是体验的分水岭——超过1秒就会觉得“卡”。开启方法很简单,你找到MySQL的配置文件(一般是/etc/my.cnf或my.ini),加上这几行:
slow_query_log = ON slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = ON # 记录未使用索引的查询
改完记得重启MySQL服务(systemctl restart mysql),不然不生效。这里有个坑要提醒你:如果是云服务器,可能需要先给日志文件授权(chown mysql:mysql /var/log/mysql/slow.log),否则MySQL没权限写入,日志会是空的。
拿到慢查询日志后,别直接打开看——几万行SQL根本没法读。用MySQL自带的mysqldumpslow
工具分析更高效,比如执行mysqldumpslow -s t /var/log/mysql/slow.log
,就能按执行时间排序,一眼看到最耗时的查询。我之前帮那个电商网站分析时,发现一条SELECT FROM orders WHERE user_id=xxx AND status=0
的查询,每天执行2000多次,每次要2.8秒,直接占满了数据库连接池。
EXPLAIN工具:让SQL“裸奔”给你看
找到慢查询后,下一步是搞清楚“为什么慢”。这时候就得请出EXPLAIN工具——它能模拟MySQL执行SQL的过程,把查询计划摊开给你看。用法超简单,在你的SQL前加EXPLAIN
就行,比如EXPLAIN SELECT FROM products WHERE category_id=1 AND price < 100;
。重点看这几列:
我举个真实案例:那个电商网站的商品列表页,原来的SQL是SELECT FROM products WHERE category_id=1 ORDER BY sales DESC LIMIT 10,20
,用EXPLAIN一看,type是ALL,rows是50000(全表5万条数据)。后来我 他们给category_id
和sales
建个复合索引,再把SQL改成SELECT id,name,price FROM products WHERE category_id=1 ORDER BY sales DESC LIMIT 10,20
(只查需要的字段),type变成了range,rows降到200,查询时间从1.2秒变成了80毫秒。
下面这个表格整理了我遇到过的常见慢查询场景和对应诊断方法,你可以对照着排查:
慢查询特征 | EXPLAIN关键指标 | 可能原因 | 诊断工具 |
---|---|---|---|
页面加载突然变慢,后台无报错 | type=ALL,rows数值大 | 缺少索引或索引失效 | EXPLAIN + 慢查询日志 |
查询偶尔超时,大部分时间正常 | key=NULL,Extra=Using temporary | 排序/分组未用索引,产生临时表 | SHOW PROCESSLIST + EXPLAIN |
批量操作(如订单导入)时系统卡死 | rows接近表数据总量 | 全表更新/删除,未加限制条件 | 慢查询日志 + 事务日志 |
表:常见慢查询场景诊断对照表
这里插一句权威参考:MySQL官方文档 通过慢查询日志和EXPLAIN结合是性能诊断的“黄金组合”,你可以看看MySQL官方关于慢查询日志的说明{:target=”_blank” rel=”nofollow”},里面有更详细的参数配置。
索引优化与SQL重构:PHP应用提速实战技巧
找到问题后,接下来就是“动手优化”。我见过很多PHP开发者觉得“优化数据库”很高深,其实80%的性能问题都能通过索引调整和SQL改写解决。就像盖房子,地基(索引)打不好,上面再花哨也站不稳。这部分我会结合自己踩过的坑,讲清楚“怎么建索引”“怎么改SQL”,还有缓存怎么用才能真正帮上忙。
索引设计:从“能用”到“好用”的关键
很多人建索引就知道加个主键,其实索引里门道多着呢。我之前接手过一个项目,用户表(users)有10万条数据,登录接口要查SELECT
FROM users WHERE username=’xxx’,居然要1秒多——一看表结构,username
字段居然没加索引!加了唯一索引后,查询直接降到10毫秒。这就是最基础的“普通索引”,但实际开发中更常见的是“复合索引”,比如商品表查category_id=1 AND status=1
,就得建(category_id, status)
这样的组合索引。
这里有个“最左前缀原则”要记牢:复合索引只会按从左到右的顺序匹配。比如你建了(a,b,c)
,那WHERE a=1
能用,WHERE a=1 AND b=2
能用,但WHERE b=2 AND c=3
就用不上了。我之前帮一个社区网站优化时,他们的帖子表索引是(created_at, user_id)
,但查询常用WHERE user_id=123 AND created_at > '2023-01-01'
,结果索引完全没生效。后来把顺序改成(user_id, created_at)
,查询时间从800ms降到了50ms。
下面是我 的“索引设计避坑指南”,你可以直接拿去用:
status
字段只有0和1两个值,建索引反而会拖慢更新速度(索引需要维护); SQL重构:让每句查询都“轻装上阵”
有时候索引没问题,但SQL写得太“臃肿”也会慢。比如你是不是经常写SELECT
?我见过一个订单详情页查询,SELECT FROM orders JOIN order_items ON orders.id=order_items.order_id WHERE orders.id=123
,结果orders
表有30多个字段,order_items
有20多个,其实页面只需要10个字段。改成只查需要的字段后,数据传输量减少70%,查询快了一倍。
分页查询也是个重灾区。很多人写LIMIT 10000, 20
,以为只取20条数据,其实MySQL会先扫描前10020条,再扔掉前10000条。我之前处理一个历史订单查询,用户要查第500页(LIMIT 50000, 20),查询要2秒多。后来改成“延迟关联”:先查主键ID,再关联取数据——SELECT o. FROM orders o JOIN (SELECT id FROM orders WHERE status=1 LIMIT 50000, 20) AS t ON o.id=t.id
,时间直接压到200ms。
还有JOIN查询,尽量用“小表驱动大表”。比如查用户订单,orders
表100万条,users
表10万条,应该用users JOIN orders
(小表在前),而不是orders JOIN users
。因为MySQL会用左表的数据去匹配右表,小表数据少,匹配次数就少。我之前有个项目反着写,结果JOIN查询要5秒,调整顺序后降到500ms。
缓存策略:给数据库“减负”的实用技巧
如果查询优化到极限还是慢,就得请缓存出场了。Redis是PHP应用的“好搭档”,尤其适合缓存热点数据——比如首页商品列表、用户个人信息、热门文章等。我通常会在PHP代码里这样用:先查Redis,如果有数据直接返回;没有再查数据库,查到后存到Redis,设置个过期时间(比如10分钟)。
举个例子,商品详情页:
$productId = $_GET['id'];
$redisKey = "product:{$productId}";
// 先查缓存
$product = $redis->get($redisKey);
if (!$product) {
// 缓存 miss,查数据库
$product = $db->query("SELECT FROM products WHERE id={$productId}")->fetch();
// 存缓存,设置10分钟过期
$redis->setex($redisKey, 600, json_encode($product));
}
echo json_encode($product);
不过缓存有个“数据一致性”问题要注意:当数据库数据更新时,要记得删缓存(而不是更新缓存)。比如商品价格改了,执行$redis->del($redisKey)
,下次查询会自动从数据库取新数据。我之前忘了删缓存,导致用户看到的还是旧价格,被投诉了好几次才发现问题。
最后分享个“笨办法”:优化完后,你可以用PHP的microtime()
函数记录查询前后的时间差,对比优化效果。比如:
$start = microtime(true);
// 你的SQL查询
$end = microtime(true);
echo "查询耗时:" . ($end
$start) . "秒";
我每次优化都会这么测,看着耗时从“1.2秒”变成“0.05秒”,成就感满满~
如果你按这些方法试了,不管是慢查询处理、索引优化还是SQL改写,欢迎回来告诉我你的项目性能提升了多少,或者遇到什么卡壳的问题,咱们一起琢磨解决!
很多人第一次听说要开慢查询日志,都会担心“会不会拖慢数据库啊?”其实你放心,影响真的很小,小到大多数时候你根本感觉不到。慢查询日志本质上就是MySQL多做了一步“记笔记”的工作——把执行超时的SQL记下来,又不是让它额外跑什么复杂计算。MySQL官方自己做过测试,就算24小时开着慢查询日志,性能损耗通常也就在1%以内,相当于你手机后台多开了个备忘录APP,基本不影响正常使用。
不过要是你实在不放心,有两个小技巧可以试试。一个是“临时开启”,比如你怀疑最近数据库有点卡,直接在MySQL命令行里输SET GLOBAL slow_query_log=ON
,排查个半小时到一小时,找到问题SQL后再用SET GLOBAL slow_query_log=OFF
关掉,全程手动控制,灵活又安全。我之前帮一个做在线教育的朋友排查问题,就是这么干的,中午12点学生下课那会儿开日志,收集到3条慢查询就关了,完全没影响下午上课的高峰期。另一个办法是“低峰期开启”,比如选凌晨2-4点这种用户最少的时候,这时候就算日志记录稍微占点资源,用户也感知不到,等天亮了业务起来前关掉就行,两边都不耽误。
如何判断一条SQL是否属于慢查询?
通常可以通过MySQL的慢查询日志来识别,默认情况下执行时间超过1秒(可通过long_query_time参数调整)的SQL会被记录为慢查询。 你也可以在PHP代码中用microtime()函数手动记录查询耗时,比如执行前记录开始时间,执行后计算时间差,超过1秒就需要重点关注。
开启慢查询日志会影响MySQL性能吗?
轻微影响,但可忽略。慢查询日志本质是记录SQL执行信息,MySQL官方测试显示,即使开启日志,性能损耗通常在1%以内。如果担心性能,可临时开启(SET GLOBAL slow_query_log=ON),排查完问题后关闭;或只在业务低峰期开启,避免对高并发场景产生影响。
建了索引但查询还是慢,可能是什么原因?
常见原因有三个:一是没遵循“最左前缀原则”,比如复合索引(a,b,c),查询条件只用b和c会导致索引失效;二是索引字段被函数操作,比如WHERE SUBSTR(username,1,3)=’abc’,索引无法使用;三是数据量过大时,索引选择性低(比如字段值重复率高,如性别字段), 结合SQL重构(如只查必要字段)或分表分库优化。
使用Redis缓存MySQL数据时,如何避免缓存和数据库数据不一致?
推荐“更新数据库后删除缓存”的策略。比如修改商品价格后,先更新MySQL,再调用Redis的DEL命令删除对应缓存key,下次查询时会自动从数据库加载新数据并重建缓存。避免直接更新缓存,因为并发场景下可能出现“缓存覆盖”问题,导致数据不一致。
EXPLAIN结果中“type”字段显示“ALL”,该怎么优化?
“ALL”表示全表扫描,说明查询未使用索引。你可以先检查WHERE条件中的字段是否有索引,没有就添加(比如给过滤字段建普通索引或复合索引);如果有索引但未使用,可能是索引失效(如字段类型不匹配、使用OR连接无索引字段),可尝试修改查询条件,比如把OR换成UNION,或确保字段类型一致(如字符串不加引号会导致索引失效)。