Hyperf数据库查询优化核心是减少查询、避免N+1、合理缓存与连接复用,并精准使用Query Builder;需明确select字段、预加载关联、用paginate/cursorPaginate分页、优先加索引而非依赖缓存。

Hyperf 的数据库查询性能优化,核心在于减少不必要的查询、避免 N+1 问题、合理利用缓存与连接复用,并善用 Query Builder 的链式能力而非拼接 SQL。不是写得越复杂越高效,而是越精准、越延迟执行、越贴近业务场景越有效。
用 select() 明确字段,别用 *
默认使用 select('*') 会拉取整行数据,即使只用其中一两个字段。尤其在关联表多、字段含大文本或 JSON 类型时,网络传输和内存开销明显上升。
- 只查需要的字段:
User::query()->select('id', 'name', 'email')->where('status', 1)->get() - 关联查询中也要限制字段:
$users = User::with(['posts' => fn($q) => $q->select('id', 'user_id', 'title')])->get(); - 注意:关联预加载的
select必须包含外键(如user_id),否则关联关系无法匹配
善用 with() 预加载,消灭 N+1
循环中调用关联模型(如 $user->posts)会触发多次 SQL 查询,100 个用户就发 100 条 SELECT * FROM posts WHERE user_id = ? —— 这是典型 N+1 问题。
- 改用预加载:
User::with('posts')->get(),底层自动转为 2 条 SQL(users + posts IN (...)) - 嵌套关联也支持:
User::with(['posts.comments.user'])->get() - 需条件过滤时用闭包:
User::with(['posts' => fn($q) => $q->where('published', 1)])->get()
分页用 paginate(),别用 offset/limit 手动分页
手动写 skip(10000)->take(20) 在大数据量下极慢,MySQL 需扫描前 10000 行。而 paginate() 默认使用游标分页(配合 cursorPaginate() 更佳)或优化后的 limit/offset,同时自动注入分页参数、生成链接。
- 基础分页:
User::where('status', 1)->paginate(15) - 游标分页(推荐用于无限滚动):
User::where('status', 1)->cursorPaginate(15),基于主键或唯一索引字段,无性能衰减 - 注意:游标分页不支持
orderByRaw或复杂排序,需确保排序字段有索引且稳定
查询前加索引,而不是查询后加缓存
缓存只是兜底手段。先确认慢查询是否因缺失索引导致——用 EXPLAIN 分析执行计划,重点关注 type=ALL(全表扫描)或 rows 过大。
- 常见索引场景:WHERE 字段(
status、created_at)、ORDER BY 字段、JOIN 关联字段、复合查询条件(如(status, created_at)) - Hyperf 中建索引可在迁移文件中写:
$table->index(['status', 'created_at']); - 避免过度索引:每个索引都会拖慢写入速度,优先覆盖高频查询路径
不复杂但容易忽略:开启 Query Log 本地调试(dev 环境下 db.logger.enabled=true),配合 DB::enableQueryLog() 和 DB::getQueryLog() 快速定位低效语句。真正高效的查询,是让数据库少做事,而不是让 PHP 多做事。









