Db::table()查询慢需先精简字段再建索引:用field()指定必要字段,WHERE字段须建索引,避免索引字段运算;预加载with()仅在真需关联数据时使用;缓存key需带业务上下文并分层设过期时间;大数据量分页应改用游标分页。

Db::table() 查询太慢?先砍字段,再加索引
直接 Db::table('users')->select() 在数据量稍大时就会明显卡顿,根本原因是 MySQL 被迫全表扫描 + 传输大量无用字段。这不是 ThinkPHP 的锅,而是没按数据库基本功来用。
- 永远用
field()明确指定字段,比如Db::table('users')->field('id,name,avatar')->where('status',1)->select() - WHERE 条件里出现的字段(如
status、created_at)必须建单列或复合索引,否则field()再干净也没用 - 别在索引字段上做运算,比如
where('DATE(created_at)','2025-01-01')会让索引失效;改用whereBetween('created_at',['2025-01-01 00:00:00','2025-01-01 23:59:59'])
with() 预加载还是 N+1?检查关联模型是否真被用了
with('posts') 看似解决了 N+1,但如果控制器里只取了用户列表、压根没遍历 $user->posts,那这次 JOIN 或子查询就是纯浪费——预加载会强制执行,不管后续用不用。
- 只在视图或逻辑中**确实需要访问关联数据**时才加
with() - 避免嵌套预加载如
with(['posts.comments.user']),容易一次拉出几十倍数据;拆成两步查更可控 - 用
Db::getLastSql()或开启 SQL 日志,确认生成的 SQL 是一条 JOIN 还是多条独立查询
cache() 缓存失效混乱?别混用 key 命名和过期策略
缓存键(key)命名随意、过期时间拍脑袋定,会导致数据陈旧、并发写入冲突、甚至缓存雪崩。ThinkPHP 的 cache() 不是开关,是需要设计的组件。
- key 必须带业务上下文,比如用户列表缓存不要用
'user_list',而用'user_list_status_1_' . md5($order) - 避免全局统一设 3600 秒,高频变动数据(如库存)缓存 60 秒,静态配置可缓存 86400 秒
- 慎用
cache(true)自动 key:它依赖序列化内容生成 hash,字段顺序微调就导致缓存不命中,调试困难
paginate() 分页卡死?游标分页比 limit offset 更可靠
当数据量超 10 万行,paginate(20) 在第 500 页(offset=9980)时,MySQL 仍要扫描前 9980 行才能取 20 条——这不是 ThinkPHP 分页逻辑的问题,是 SQL 本身的缺陷。
立即学习“PHP免费学习笔记(深入)”;
- 对「按时间倒序」这类场景,改用游标分页:
where('id','limit(20),性能几乎恒定 - 确保游标字段(如
id或created_at)有索引,且值唯一、单调递增/递减 - ThinkPHP 本身不内置游标分页,但你可以封装一个
cursorPaginate()方法,复用现有查询构造器
真正卡住性能的,往往不是框架有多重,而是开发者把 ORM 当黑盒,绕开了数据库最基础的约束与成本意识。索引、字段、缓存 key、分页方式——这些词背后全是 MySQL 的执行计划和内存开销,不是 PHP 层能掩盖的。


















