Hyperf 中 select(*) 会显著增加内存占用并拖慢接口,因其协程常驻特性导致未释放的冗余字段(如大文本、JSON)持续累积,引发内存溢出;连表查询、ORM映射、缓存及游标读取均受字段数量直接影响,必须显式精简字段并严格控制关联查询。

为什么 Hyperf 里用 select(*) 会吃内存还拖慢接口
不是 Laravel 那套“习惯性写法”在 Hyperf 里还能蒙混过关。Hyperf 的协程模型对内存更敏感,select(*)(比如 Db::table('users')->get() 或 User::all())会把整行字段全拉回来,哪怕你只用 id 和 name。大文本、JSON、冗余关联字段全进 PHP 内存,协程 Worker 进程常驻,不释放就累积——轻则响应变慢,重则 Fatal error: Allowed memory size of ... exhausted 直接崩。
- 数据库传输量翻倍:尤其连表后,
orders JOIN order_items可能从 10 万行变成 50 万行结果集 - ORM 映射开销上升:Hyperf 的
Collection要为每个字段建属性,字段越多越慢 - 缓存失效更频繁:Redis 存的 JSON 包含大量不用字段,体积大、更新成本高
select() 和 addSelect() 别混用,顺序和语义必须分清
select() 是覆盖式重置字段列表,addSelect() 是追加,两者行为完全不同。新手常写 ->select('id')->select('status'),结果只有 status;或者想给主查询加统计字段,却误用 select() 把主字段全冲掉了。
-
select('id', 'title')后再调select('created_at')→ 只剩created_at - 要加聚合或别名字段,必须用
addSelect():->select('user_id')->addSelect(Db::raw('COUNT(*) as cnt')) -
selectRaw()本质是addSelect()的语法糖,但别指望它能替代主字段声明 - 关联预加载(
with())不继承主查询的select(),必须单独约束:->with(['profile' => fn($q) => $q->select('user_id', 'avatar')])
Hyperf 模型 + 原生查询字段控制要分开处理
Hyperf 的 Model 和 Db::table() 虽然都支持 select(),但底层机制不同,容易漏掉一端。比如你在 User::on('read_pool_1') 上做了字段精简,但 with(['posts']) 里的 Post 模型没切读库也没限制字段,照样走默认连接、查全字段。
-
User::select('id','name')->on('read_pool_1')->with(['posts' => fn($q) => $q->select('id','title')->on('read_pool_1')])—— 主从都要显式控制 -
Db::table('users')->select('id','email')->connection('read_pool_1')->get()—— 原生查询必须先connection()再链式调用 -
Db::select()不接受连接名参数,必须Db::connection('xxx')->select(...) - 字段少了,注意
$casts和访问器可能不触发:比如没选content字段,getFormattedContentAttribute()就不会跑
游标读取时字段控制比平时更关键
用 cursor() 或手动 PDO::MYSQL_ATTR_USE_BUFFERED_QUERY = false 流式读取大数据集时,字段越少,单行内存占用越低,GC 压力越小。这时候 select(*) 不只是慢,是直接卡死进程。
- 连表查询务必只选业务需要的字段,避免
SELECT u.*, o.*这种写法 - 大字段(如
TEXT、JSON)坚决不选,除非当前循环逻辑真要用到 - 游标循环中
unset($row['big_field'])是必要操作,别依赖 GC 自动回收 - Hyperf 3.1.66+ 的
cursor()方法已支持流式,但前提是底层 SQL 已明确select()字段,否则还是缓冲全量
select(),不代表子查询也安全了。



















