select(*) 在 Laravel 中是性能隐患,因强制返回整行数据(含大文本、JSON 等冗余字段),导致网络传输增加、内存占用上升、ORM 映射变慢,分页或高并发下尤为明显。

为什么 select(*) 在 Laravel 中是性能隐患
不是所有字段都参与后续逻辑,但 select(*) 会强制数据库返回整行数据,包括大文本字段(如 content、description)、JSON 字段、冗余关联字段。这直接导致:网络传输量增加、内存占用上升、ORM 映射变慢,尤其在分页或高并发场景下更明显。
常见错误现象:DB::table('posts')->get() 或 Post::all() 默认查全字段;Eloquent 关联中未限制子查询字段,引发 N+1 的“加量版”问题(查出一堆不用的字段)。
- 使用场景:列表页、API 接口、后台导出前的数据预处理
- 参数差异:
select(*)是隐式行为,select()是显式控制入口 - 兼容性影响:Laravel 5.5+ 全系列支持,无版本风险;但若模型含
$casts或get*Attribute访问器,只选部分字段时可能跳过某些自动转换逻辑(比如json字段没被选中,就不会触发解码)
select() 和 addSelect() 怎么用才不翻车
select() 是重置字段列表,addSelect() 是追加字段(常用于子查询或聚合)。新手常误以为 select('id', 'title') 后再调 with('user') 还能拿到 user.name —— 实际不能,关联关系加载走的是独立查询,和主查询的 select 无关。
正确做法是分别控制主表和关联表字段:
Post::select('id', 'title', 'created_at')
->with(['user' => function ($q) {
$q->select('id', 'name', 'avatar');
}])
->get();
- 避免写
select('id', 'title')->select('id', 'status'),后者会覆盖前者 - 如果要用原生 SQL 别名(如
selectRaw('COUNT(*) as total')),必须配合addSelect()或单独用selectRaw,否则会被主select()清掉 - 注意
distinct和groupBy场景下,所有SELECT字段必须出现在GROUP BY中,Laravel 不校验这点,容易报错SQLSTATE[42000]: Syntax error or access violation
关联预加载时字段没生效?检查 with() 里的闭包是否写了 select()
很多人以为给主模型 select() 就够了,结果日志里还是看到完整 users.* 查询。这是因为 with() 默认发起的是完整子查询,必须显式约束。
典型错误:Post::select('id', 'title')->with('user')->get() → users.* 照查不误。
正确写法(两种):
// 方式一:闭包内 select
Post::select('id', 'title')->with(['user' => function ($q) {
$q->select('id', 'name');
}])->get();
// 方式二:在关联定义里加 local scope(适合复用)
// Post.php 模型中
public function user()
{
return $this->belongsTo(User::class);
}
// 再建一个 scope:
public function scopeWithUserBrief($query)
{
return $query->with(['user' => function ($q) {
$q->select('id', 'name', 'email');
}]);
}
- 闭包里漏写
id字段会导致关联失败(外键匹配不上),这是最常踩的坑 - 如果关联表用了软删除,且你没选
deleted_at字段,withTrashed()之类的方法可能失效 - MySQL 8.0+ 支持
SELECT ... FROM JSON_TABLE,但 Laravel 的select()不处理 JSON 内部字段投影,别指望靠它优化 JSON 字段解析开销
复杂查询里字段选择容易被中间件/作用域悄悄覆盖
全局作用域(Global Scopes)、本地作用域(Local Scopes)、查询构建器中间件(如多租户 tenant scope)可能内部调用了 select(*) 或未适配字段白名单。一旦启用,你写的 select('id') 可能被后面执行的作用域覆盖或忽略。
排查方法:开启查询日志,看最终 SQL 是否如你所写;或用 toSql() 预览:
$sql = Post::select('id')->with('user')->toSql();
// 输出类似:select `id` from `posts` where `posts`.`deleted_at` is null
// 如果没看到 `id`,说明某处 scope 强制改写了 select
- 全局作用域里慎用
select(),优先用where控制可见性,而非字段裁剪 - 自定义作用域函数若接受字段参数,需明确文档化“该作用域不保证字段安全”,避免使用者误以为加了它就自动优化了 select
- 第三方包(如 laravel-tenancy、laravel-scout)可能注入自己的查询逻辑,字段选择要以最终生成的 SQL 为准,不能只信链式调用顺序
字段选择不是加个 select() 就万事大吉的事,它和作用域、关联加载、数据库特性耦合得很紧。最容易被忽略的,是那个没写进闭包里的 id,以及你以为关掉了的全局作用域。











