慢查询排查需聚焦高频重复SQL而非单次耗时,用DB::listen()全局捕获带参数的真实SQL,写入独立日志并按频次排序优化;验证索引须代入真实参数EXPLAIN,联合索引顺序需匹配WHERE条件,外键与软删除字段需手动建索引,加索引应避免锁表。
慢查询日志本身不等于性能瓶颈,真正卡住页面的常是“单次不慢、但高频重复执行”的 SQL,比如循环里查头像——每条
耗时 15ms,100 次就是 1.5 秒。
DB::listen() 记录真实执行上下文,别只开 enableQueryLog()
只在当前请求生命周期内有效,且不记录绑定参数的实际值,调试时容易误判。而
是全局监听器,能捕获所有连接的完整上下文。
必须在
中注册,否则中间件或命令行场景会漏掉
日志要写入独立文件(如
),避免和业务日志混在一起难 grep
务必拼接
和
,用
替换问号,否则无法在 phpMyAdmin 里准确复现
别设固定阈值(如
),先按频次排序:同一
出现 47 次,比一条耗时 2800ms 的后台统计更值得优先优化
grep 日志定位高频 SQL 时注意预处理语句的变形
日志里看到的
是占位符形式,不代表实际执行逻辑。MySQL 优化器对不同参数组合可能走不同执行计划。
用
提取所有相关行,再人工合并看参数分布(比如
出现 32 次,
只有 1 次)
如果某类查询总 rows 数远超表总行数(如
显示
,而表只有 5000 行),说明索引失效,不是语句写得慢,而是没走索引
带
的分页查询,若
没单独索引或不在联合索引最右,
会出现
,这是典型信号
phpMyAdmin 里验证索引必须传真实参数,不能直接跑带问号的 SQL
把日志里抄来的
直接粘贴进 phpMyAdmin 执行
,结果
并不意味着线上也全表扫描——因为 MySQL 没法基于问号做执行计划估算。
从日志中挑出具体一次调用,例如
,替换成完整 SQL 再
联合索引顺序必须匹配 WHERE 条件顺序:
有效,
对该查询无效
软删除字段
如果参与查询(如
),需单独加索引或包含在联合索引中,否则
无法使用 B-tree 索引
外键字段(如
)Laravel 不自动建索引,
查询必扫全表,必须手动加
大表加索引要绕过锁表,迁移里别直接用 addIndex()
在百万级订单表上执行,MySQL 5.7 默认会锁表,用户请求全部阻塞。
改用原生 SQL +
(MySQL 5.6+):
加索引前先
确认当前引擎支持在线 DDL(InnoDB 支持,MyISAM 不支持)
关联查询中的隐式条件容易被忽略:比如
会生成
,
必须有索引
高频查询的真实参数分布、索引是否命中、加索引时是否锁表——这三件事串起来才构成完整的慢查询排查闭环。漏掉任何一个,都可能让优化效果打折扣甚至倒退。
SELECT * FROM users WHERE id = ?DB::enableQueryLog()DB::listen()AppServiceProvider::boot()storage/logs/queries.log$query->sql$query->bindingsvsprintf()time > 1000SELECT * FROM comments WHERE post_id = ?SELECT * FROM posts WHERE user_id = ? AND status = ?grep -n "posts.*user_id" storage/logs/queries.loguser_id = 123user_id = 999EXPLAINrows=124800ORDER BY created_at DESC LIMIT 20created_atExtraUsing filesortSELECT * FROM orders WHERE status = ? AND paid_at > ?EXPLAINtype=ALLstatus = 'paid' AND paid_at > '2026-04-20'EXPLAIN SELECT ...INDEX(status, paid_at)INDEX(paid_at, status)deleted_atwhere deleted_at is nullIS NULLuser_idbelongsToSchema::table('orders', function (Blueprint $table) { $table->index('status'); });ALGORITHM=INPLACEDB::statement("ALTER TABLE orders ADD INDEX idx_orders_status_paid (status, paid_at) ALGORITHM=INPLACE")SHOW CREATE TABLE orderswithCount('comments')SELECT COUNT(*) FROM comments WHERE post_id = ?post_id