必须用getlastsql()获取真实sql再执行explain验证索引:type非all、key非null且为预期索引、rows接近实际返回行数;避免左模糊、函数操作、or查询及隐式类型转换等导致失效的写法。

怎么确认ThinkPHP查询到底有没有走索引
别信 buildSql() 输出的 SQL,它只拼字符串,不带真实参数,更看不出索引用没用上。真正要验证,得靠 getLastSql() + MySQL 的 EXPLAIN。
操作步骤很直接:
- 执行完查询后立刻调用
$query->getLastSql(),拿到已替换参数的真实 SQL(比如WHERE mobile = '138****1234') - 把这句 SQL 复制到 MySQL 客户端,在前面加
EXPLAIN后执行 - 重点盯三个字段:
type不能是ALL;key不能是NULL,且得是你建的索引名;rows越小越好,接近实际返回行数才算健康
常见假象:明明给 status 加了单列索引,但 where(['status'=>1, 'score'=>['>', 80]]) 还是全扫——因为范围条件后面的字段无法利用单列索引,必须建 (status, score) 联合索引。
哪些ThinkPHP写法会悄悄让索引失效
ORM 链式调用掩盖了底层 SQL 的危险操作,一写错,索引就形同虚设,而且不报错、不提示。
典型踩坑点:
-
where('name', 'like', '%abc')→ 左模糊,B+ 树没法前缀匹配,key必为NULL;应改用where('name', 'like', 'abc%')或全文索引 -
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('a', 1)->whereOr('b', 2)→ 生成WHERE a = 1 OR b = 2,通常两个单列索引都用不上;优先改写为UNION或补联合索引
隐式类型转换也危险:数据库字段是 VARCHAR,却传整型值 where('mobile', 13800001234),MySQL 会触发全表扫描;必须统一为字符串 where('mobile', '13800001234')。
field() 不是可选项,是必填项
select() 默认查所有字段,哪怕你只用 $user['name'],宽表 + TEXT 字段时,MySQL 可能被迫走临时磁盘表,PHP 还得多解析几十 KB 数据。
必须显式收敛字段:
- 主表查询:用
field('id,name,avatar,status')替代留空或field('*') - 字段含关键字(如
order、group)得加反引号:field('`order`, status') - 关联预加载也要单独控字段:
with(['profile' => function ($q) { $q->field('user_id,real_name'); }]) - 列表页和详情页字段分离:列表只取
ID + 标题 + 时间;详情再补全或按需field()
不收敛字段,即使条件走了索引,也可能因回表(fetching non-indexed columns)拖慢整体响应。
paginate() 是大数据量下的性能陷阱
paginate(20) 查第 500 页时,MySQL 仍要扫描前 9980 行才能取 20 条——这不是 ThinkPHP 的 bug,是 LIMIT offset, size 的固有缺陷。
游标分页才是解法:
- 用
WHERE id > ? ORDER BY id LIMIT 20替代LIMIT 9980, 20 - 确保
id有索引(主键一般满足) - 前端传上一页最后一条的
id值,作为下一页查询起点 - 不能跳页,但翻页响应从秒级降到毫秒级
复合条件游标分页稍复杂,比如按 created_at 排序,就得组合 WHERE (created_at, id) > (?, ?),否则时间相同会导致漏数据——这点容易被忽略,但直接影响数据完整性。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











