需开启app_debug=true并在database.php配置log级别为sql,结合bind参数分析真实SQL;用EXPLAIN重点检查key、rows、Extra三列,避免N+1和大数组whereIn,分页慎用COUNT。

怎么打开 ThinkPHP 的 SQL 日志并定位慢查询
ThinkPHP 默认不输出完整 SQL,光看日志根本不知道哪条查得慢。得手动开调试开关,并把日志落到文件里才能分析。
-
app_debug必须设为true,否则连 SQL 都不记录 - 在
config/database.php里加'log' => ['level' => ['sql']],只记录 SQL 级别日志,避免干扰 - 日志默认写到
runtime/log/下,但要注意:如果用了 Swoole 或常驻进程,日志可能不实时刷盘,得加ob_flush()或手动flush() - 常见错误现象:
SQL: SELECT * FROM user WHERE id = ?这种带问号的不是真实 SQL,得配合bind参数一起看,否则看不出实际执行条件
explain 看不出索引生效?重点盯这三列
ThinkPHP 生成的 SQL 拿去 MySQL 执行 EXPLAIN 后,不能只扫一眼 type 是不是 ref 或 range —— 很多优化卡在细节上。
-
key列为空?说明没走索引,哪怕你建了索引,也可能是字段类型不匹配(比如user_id是VARCHAR,但查询时传了整数) -
rows值远大于实际结果数?说明索引选择性差,或者用了函数(如WHERE YEAR(create_time) = 2024),导致索引失效 -
Extra出现Using filesort或Using temporary?基本等于要重写查询逻辑,尤其是ORDER BY和GROUP BY混用时,ThinkPHP 的order()+group()容易触发这个
ThinkPHP 的 where() 写法直接影响执行计划
同一个业务逻辑,where() 写法不同,生成的 SQL 可能完全不一样,MySQL 优化器处理方式也不同。
- 用
where('status', 1)生成的是status = 1,没问题;但写成where('status = 1')(字符串形式)会绕过参数绑定,可能被注入,且某些版本 TP 不会走预编译,影响执行计划缓存 -
whereIn('id', $array)如果$array超过 1000 个元素,MySQL 会拒绝执行或退化成全表扫描,得手动分批或改用临时表 - 关联查询慎用
with(),它默认是 N+1 查询;真要联查,得用join()显式写出,否则EXPLAIN看不到 JOIN 类型,优化无从下手
count() 和分页为什么越来越慢
ThinkPHP 的 paginate() 默认先执行 SELECT COUNT(*),再查数据。当表大、条件复杂时,这个 COUNT 本身就成了瓶颈。
立即学习“PHP免费学习笔记(深入)”;
- 如果只是展示“大概多少条”,可以用
SHOW TABLE STATUS查Rows字段近似值,TP 里可手动执行Db::query('SHOW TABLE STATUS LIKE "user"') - 带
JOIN和GROUP BY的分页,COUNT几乎必慢;要么改用游标分页(WHERE id > ? ORDER BY id LIMIT 20),要么把统计逻辑拆到定时任务里缓存 - TP 6.1+ 支持
paginate(false)跳过 count,但前端必须接受“没有总页数”,这不是偷懒,是权衡
真正卡住性能的,往往不是语法对不对,而是 explain 里 rows 和 Extra 的组合含义没人细看;还有就是开发时用小数据测不出问题,一上生产,whereIn 传几千 ID 或 with 套三层关联,直接拖垮数据库连接池。


















