ThinkPHP查询慢主因是SQL未走索引、字段冗余、缓存配置不当;应通过getLastSql()+EXPLAIN验证索引使用(看type非ALL、key非NULL、rows小),避免左模糊和函数操作,建联合索引须遵循最左匹配,并强制field()收敛字段、改用游标分页。

ThinkPHP 查询慢,八成不是框架拖后腿,而是 SQL 没走索引、字段拉太多、缓存没开对——改这三块,800ms 常能压到 80ms。
怎么确认查询到底有没有走索引
别信 buildSql() 输出的 SQL 看着“干净”就以为没问题。它只拼字符串,不执行,更不告诉你用没用上索引。
buildSql() 返回的是带问号占位符的语句(如 WHERE name = ?),得先用 getLastSql() 拿到真实值替换后的 SQL;把结果复制进 MySQL 客户端,前面加 EXPLAIN 执行,重点看:
-
type:非ALL才算走了索引 -
key:非NULL表示实际用了哪个索引 -
rows:越小越好,接近表总行数基本等于全扫
常见假象:WHERE status = 1 AND score > 80,如果只建了 (status) 单列索引,score 那段就失效;必须建 (status, score) 联合索引才有效。
立即学习“PHP免费学习笔记(深入)”;
哪些 ThinkPHP 写法会悄悄让索引失效
ORM 链式调用掩盖了底层 SQL 的危险操作,一写错,索引直接作废:
-
where('name', 'like', '%abc')—— 左模糊,B+ 树没法前缀匹配,key必为NULL -
whereRaw('DATE(create_time) = "2024-01-01"')—— 对字段套函数,索引失效;应改写为whereBetween('create_time', ['2024-01-01 00:00:00', '2024-01-01 23:59:59']) -
where('user_id', $uid)->order('id desc')->limit(10)—— 若id无索引,Extra字段会出现Using filesort,排序变全表扫描 -
where('category_id', 5)->where('status', 1)->where('score', '>', 70)—— 复合索引必须按顺序覆盖:(category_id, status, score)可用;(category_id, score)则status后面的score就断了
field() 不是可选项,是必填项
select() 默认查所有字段,哪怕你只用 $user['name']。宽表 + TEXT 字段时,MySQL 可能被迫走临时磁盘表,PHP 还要多解析几十 KB 数据。
- 必须显式调用
field('id,name,avatar'),禁止field('*')或留空 - 字段含关键字(如
order、group)得用反引号:field('`order`, status') - 关联预加载也要单独控字段:
with(['profile' => function ($q) { $q->field('user_id,real_name'); }]) - 列表页和详情页字段分离:列表只取
ID + 标题 + 时间;详情再补全或按需field()
paginate() 在大数据量下就是个陷阱
paginate(20) 到第 500 页时,MySQL 仍要扫描前 9980 行才能取 20 条。这不是 ThinkPHP 的 bug,是 LIMIT offset, size 的固有缺陷。
- 真要分页,优先用游标分页:
WHERE id > ? ORDER BY id LIMIT 20,且确保id有索引 - 关闭自动
COUNT(*):paginate(15, false, ['query' => request()->param()]),自己用 Redis 缓存总数或覆盖索引估算 - 前端限制最大页码,后端加判断:
if ($page > 2000) { throw new HttpException(400); }
最易被忽略的一点:软删除字段 delete_time 必须加索引,否则 where('delete_time', null) 也会全表扫;还有主键 id 被误删后,InnoDB 会自建隐藏聚簇索引,性能反而更差。



















