Laravel Model::whereIn查询慢于DB::whereIn的问题咨询
Laravel Eloquent 与 DB 门面同逻辑查询性能差异说明及优化方案
问题场景
相同查询逻辑下两种实现耗时差达到7倍以上:
- Eloquent模型查询耗时2.3秒,代码如下:
Click::whereIn( 'campaign_id', [162, 165, 189, 190, 191, 195, 196, 202, 203, 208, 209] )->get()

- DB门面直接查表耗时仅300毫秒,代码如下:
DB::table('clicks')->whereIn( 'campaign_id', [162, 165, 189, 190, 191, 195, 196, 202, 203, 208, 209] )->get()

性能差距核心原因
- 模型实例化的累计开销:DB门面返回的是无附加逻辑的轻量StdClass对象,属性填充仅做简单的字段映射;Eloquent返回的每一条结果都是完整的
Click模型实例,单条数据要完成属性类型转换、隐藏/可见字段过滤、访问器注册、关联关系配置、事件监听绑定全流程。点击日志类表通常查询结果集行数较大,逐行实例化的累计开销会被放大,这是两者最核心的性能差来源。 - 隐式SQL逻辑不一致:多数开发者默认两者生成的SQL完全相同,实际上Eloquent会自动挂载模型注册的全局作用域,最常见的包括软删除自动拼接
deleted_at IS NULL条件、多租户场景自动追加租户ID过滤。如果这些自动追加的条件没有命中对应索引,或者作用域内嵌套了低效子查询,SQL本身的执行速度就会远慢于DB门面生成的无修饰原生SQL。 - 附加处理逻辑的额外消耗:Eloquent默认会自动处理
created_at/updated_at时间字段的格式化,如果模型配置了$appends追加虚拟属性、定义了大量字段访问器、注册了模型事件监听器,每一行结果处理时都会自动触发这些逻辑,进一步拉长总耗时。
Eloquent 查询优化方案
- 第一步先校验SQL一致性:开启查询日志打印两种写法最终执行的SQL语句,确认是否存在Eloquent自动追加的慢查询条件。如果是不需要的全局作用域,查询时调用
withoutGlobalScopes()跳过即可,注意软删除、多租户类作用域跳过前要确认业务逻辑允许:
Click::withoutGlobalScopes() ->whereIn('campaign_id', [162, 165, 189, 190, 191, 195, 196, 202, 203, 208, 209]) ->get();
- 大结果集场景跳过模型实例化:如果查询仅用于数据统计、批量导出,不需要使用模型的事件、关联、访问器能力,直接调用
toBase()方法,让Eloquent返回和DB门面完全一致的StdClass结果集,跳过模型hydration(模型填充)流程,性能可以直接追平DB门面查询:
// 无模型实例化开销,返回结果和DB::table查询一致 Click::whereIn('campaign_id', [162, 165, 189, 190, 191, 195, 196, 202, 203, 208, 209]) ->toBase() ->get();
- 按需加载字段精简处理量:不要默认使用
select *查询全字段,调用select()指定业务实际需要的字段,既减少数据库IO和网络传输量,也能降低模型属性填充、转换的开销:
Click::whereIn('campaign_id', [162, 165, 189, 190, 191, 195, 196, 202, 203, 208, 209]) ->select('id', 'campaign_id', 'click_time', 'user_id') ->get();
- 大结果集用分块处理降低峰值开销:查询结果超过千条时,不要一次性加载全量数据到内存,使用
lazy()方法做游标分块处理,逐行加载处理数据,同时降低内存占用和实例化峰值压力:
Click::whereIn('campaign_id', [162, 165, 189, 190, 191, 195, 196, 202, 203, 208, 209]) ->lazy() ->each(function ($click) { // 逐行处理业务逻辑 });
- 纯查询场景关闭非必要逻辑:不需要触发模型事件的纯查询场景,用
withoutEvents()跳过事件绑定和触发流程;不需要自动时间戳格式化的场景,临时关闭模型时间戳自动处理,进一步压缩开销。
内容的提问来源于stack exchange,提问作者Axel
相关产品推荐
相关产品推荐

