ThinkPHP查询慢八成因未走索引、字段过多或写法不当;应先用SHOW INDEX检查mobile等字段索引,添加单列或联合索引(等值在前、范围在后),避免函数操作导致失效,结合EXPLAIN和慢日志定位真实SQL与耗时,强制field()收敛字段,慎用with()和paginate(),高并发下可手动PDO预编译。

ThinkPHP 查询慢,八成不是框架问题,而是 SQL 没走索引、字段拉太多、或者查法本身有硬伤。别急着换数据库或加缓存,先看执行计划和真实耗时。
怎么确认是不是没走索引
很多同学一看到 Db::table('user')->where('mobile', $phone)->find() 延迟高,就去改 PHP 代码,其实该查的是 MySQL 索引。不建索引,哪怕只有一万条数据,WHERE mobile = ? 也会全表扫描。
- 用
SHOW INDEX FROM user查mobile是否在索引列表里;没有就立刻加:ALTER TABLE user ADD INDEX idx_mobile (mobile) - 复合条件如
where(['status' => 1, 'created_at' => ['>=', $time]]),要建联合索引,顺序必须是等值字段在前、范围字段在后:ADD INDEX idx_status_created (status, created_at) -
WHERE DATE(created_at) = '2024-01-01'这种写法会让索引失效,得改成whereBetween('created_at', [$start, $end])
怎么看到真实 SQL 和执行时间
光看 Db::getLastSql() 没用,它不带参数、不显示耗时。不亲眼看到 MySQL 实际执行了什么、花了多久,优化就是拍脑袋。
- 在
config/database.php的数据库配置里加'sql_explain' => true,每条SELECT都会自动跑EXPLAIN并打印结果 - 临时开 MySQL 慢日志:
SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 0.1;,再复现请求,去 MySQL 的 slow log 里找真凶 - 日志里看到的
?不是真实值,得配合bind参数一起看,否则看不出 WHERE 条件到底是什么
怎么避免 N+1 和大字段拖慢查询
查 100 个用户,再对每个用户调 $user->profile,就是典型的 N+1;查全字段(比如带 TEXT 或 JSON)会让磁盘、网络、内存三重吃紧。
立即学习“PHP免费学习笔记(深入)”;
- 永远用
field()显式指定字段:Db::table('users')->field('id,name,avatar,status')->where('status', 1)->select() -
with('profile')必须配field(),否则关联表也查全字段;更稳妥是在模型关联方法里直接写死:return $this->hasOne(ProfileModel::class)->field('user_id,real_name,avatar') - 分页慎用
paginate()查第 500 页,OFFSET 9980会让 MySQL 扫描前 9980 行——改用游标分页,WHERE id > ? ORDER BY id LIMIT 20,且确保id有索引
什么时候该绕过 ORM 直接用 PDO 预编译
ThinkPHP 默认的 Db::table()->where()->select() 是字符串拼接 SQL,每次都要解析、生成执行计划。高并发下,这个开销明显。但预编译不是万能药,只对结构固定、参数位置不变的查询有效。
- 获取底层 PDO:
$pdo = Db::connect()->getPdo() - 手动 prepare:
$stmt = $pdo->prepare('SELECT * FROM user WHERE mobile = ? AND status = ?') - execute 传参:
$stmt->execute([$phone, 1]); $result = $stmt->fetchAll(PDO::FETCH_ASSOC) - 如果 SQL 动态拼了
ORDER BY或LIMIT,PDO 无法复用执行计划,反而可能更慢
真正卡住 ThinkPHP 查询的,往往不是语法多复杂,而是索引漏建、字段没收敛、日志没打开、执行计划没看——这些地方不动,加再多缓存也没用。



















