ThinkPHP查询构建器性能优化核心是遵循数据库规则:必须用field()显式指定字段,WHERE条件字段须建索引,避免函数操作和左模糊导致索引失效,调试用getLastSql()+EXPLAIN验证执行计划,大数据量分页改用游标分页。

ThinkPHP 查询构建器本身不慢,慢的是没按数据库规则用——字段不选、索引不加、条件乱写,再流畅的链式调用也救不了全表扫描。
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()
where 条件一写错,索引直接失效
where() 看似简单,但 MySQL 对函数、模糊、范围查询极其敏感。ThinkPHP 不拦截这些写法,它只负责拼 SQL。
-
where('name', 'like', '%abc')→ 左模糊,B+ 树无法前缀匹配,索引失效 -
whereRaw('DATE(create_time) = "2024-01-01"')→ 对字段做函数,索引失效;改用whereBetween('create_time', ['2024-01-01 00:00:00', '2024-01-01 23:59:59']) -
where('status', 1)->where('score', '>', 80)→ 复合索引中status是等值、score是范围,索引只能用到status;若常一起查,建联合索引(status, score) - 软删除字段
delete_time必须加索引,否则where('delete_time', null)也会全表扫
buildSql() 和 getLastSql() 别混用
buildSql() 返回带问号占位符的 SQL,不能直接丢给 EXPLAIN;getLastSql() 才是替换完参数的真实语句,才是调优依据。
立即学习“PHP免费学习笔记(深入)”;
- 调试慢查时,先执行
$query->getLastSql(),复制结果到 MySQL 客户端跑EXPLAIN -
EXPLAIN看关键三列:type(非ALL才算走索引)、key(实际用的索引名)、rows(扫描行数,越小越好) - 别信
buildSql()输出“看着简洁”就以为高效——type=ALL和key=NULL才是真相 - 开启 SQL 日志后,
getLastSql()比日志更准,因为日志可能被并发请求覆盖或截断
paginate() 在大数据量下就是个陷阱
paginate(20) 到第 500 页时,MySQL 仍要扫描前 9980 行才能取 20 条。这不是 ThinkPHP 的 bug,是 LIMIT offset, size 的固有缺陷。
- 数据量超 10 万行,必须切游标分页:
where('id', 'order('id desc')->limit(20) - 游标字段(如
id或created_at)必须有索引,且值单调、唯一;created_at有重复时得组合(created_at, id) - ThinkPHP 不内置游标分页,但可封装
cursorPaginate()方法复用现有查询构造逻辑 - 别在游标分页里混用
order('id asc')和where('id', '>', $lastId)——方向反了会漏数据
最易被忽略的点:索引不是加了就生效,而是得匹配查询模式。一个 where('user_id', $uid)->order('created_at desc'),索引必须是 (user_id, created_at),反过来就白搭;而 field() 控制的是传输和解析成本,和索引无关但同样致命。



















