thinkphp查询卡死主因是mysql全表扫描,源于缺失索引、sql写法不当(如函数操作、前导like)、大offset分页、n+1查询及未预编译sql;需用explain验证、建合适索引、改写sql、限制分页、精简关联、批量操作并开启配置优化。

ThinkPHP 查询卡死,八成是 MySQL 在全表扫描——不是框架慢,是没索引、写了没法走索引的 SQL,或者分页 offset 大到离谱。
WHERE 字段没建索引,Db::table()->where() 就是全表扫
比如 Db::table('user')->where('mobile', $phone)->find() 响应超 500ms,先别加缓存,直接查索引:SHOW INDEX FROM user。如果 mobile 不在结果里,立刻加:ALTER TABLE user ADD INDEX idx_mobile (mobile)。
复合条件更要注意顺序:查 where(['status' => 1, 'created_at' => ['>=', $time]]),索引必须是 (status, created_at),反过来就失效。别在索引字段上套函数,where('DATE(created_at)', '2024-01-01') 会让整个索引作废,改用 whereBetween('created_at', [$start, $end])。
- 用
EXPLAIN看执行计划,type = ALL就是铁证 -
LIKE '%关键词'前导通配不走索引,能改LIKE '关键词%'就改;不能改就别硬扛,上 fulltext 或 ES - 字段有
TEXT/BLOB时,SELECT *容易触发磁盘临时表,IO 翻倍
paginate() 传大页码,MySQL 在原地数行数
paginate(15) 默认会先跑 COUNT(*),再 LIMIT offset, 15。当用户传 page=999999,MySQL 得先扫完前 14999970 行才能取 15 条——哪怕有索引,B+ 树深度遍历也扛不住。
别只校验 page 是正整数,要硬限制:if ($page > 1000) throw new HttpException(400);。更稳妥的是关掉自动 count:paginate(15, false, ['total' => $estimated]),用 Redis 缓存预估总数,或走覆盖索引如 SELECT COUNT(id) FROM user WHERE status = 1(前提是 id 非空且有索引)。
- 永远不要把
input('offset')直接喂给offset() - 超 10 万数据,优先考虑游标分页(
WHERE id > ? ORDER BY id LIMIT 15),跳过 offset - 高频分页接口建议缓存整页结果:
cache('list_page_' . md5($key), $data, 300)
with() 看似省事,实际可能拉垮整张表
with('profile') 会强制执行关联查询,哪怕你最后根本没访问 $user->profile。100 个用户,就多 100 次单条查询(N+1),或一次 JOIN 拉回全部 profile 字段——如果 profile 表有 content TEXT,内存直接爆。
真要用,得精简:with(['profile' => function ($q) { $q->field('id, user_id, avatar'); }])。超过两层嵌套(如 with(['posts.comments.user']))一律拆开:先查 posts ID 列表,再 where('id', 'in', $ids) 批量查 comments,再同理查 user。
- 外键字段(如
posts.user_id)必须有索引,否则 JOIN 效率比 N+1 还差 - 只展示关联名称?用子查询更轻:
field('user.*, (SELECT name FROM role WHERE role.id = user.role_id) as role_name') -
withCount()查统计,大数据量下COUNT(*)本身就很慢,优先冗余计数字段 + 事件更新
连接池打满、SQL 没预编译,高并发下直接连不上库
循环里写 foreach ($ids as $id) { Db::name('user')->where('id', $id)->find(); },等于反复创建销毁 PDO 连接,连接池很快耗尽。同样,Db::execute("UPDATE user SET status = 1 WHERE id = " . $id) 拼 SQL,不仅 SQL 注入风险高,还绕过了预编译,每次都要解析执行计划。
批量操作必须用 Db::insertAll()、Db::update() 带 where in;高并发固定结构查询,手动拿 PDO 预编译:$pdo = Db::connect()->getPdo(); $stmt = $pdo->prepare('SELECT * FROM user WHERE mobile = ? AND status = ?');。
- 预编译只对「SQL 结构不变」有效,动态拼
ORDER BY或LIMIT反而更慢 - 上线前务必关
app_debug => false,并运行php think optimize:config和php think optimize:schema - 开启
'sql_explain' => true数据库配置,每条 SELECT 自动带EXPLAIN输出
最常被忽略的点:索引不是建了就完事,得看 EXPLAIN 里 key 列是否命中、rows 是否显著下降;分页不是加个 paginate() 就安全,得控住 page 上限和 count 成本;with() 不是语法糖,是明确的 SQL 生成指令——它跑出来的每一条 JOIN,都得你来为它的性能兜底。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











