Laravel中关联查询decimal字段(whereHas)耗时过长的优化咨询
这个问题我之前在项目里碰到过,用whereHas处理hasOne关联的过滤时,数据量大了确实会慢得离谱——本质是whereHas默认生成的子查询在关联表没索引或者数据量上去后,数据库得做大量的全表扫描或者嵌套查询。下面给你几个能直接兼容你当前链式查询+分页架构的优化方案,按优先级推荐:
慢查询90%的原因都是缺少索引,你现在的查询需要同时匹配product_id(关联字段)和price(过滤字段),所以给这两个字段加联合索引是最优选择:
CREATE INDEX idx_price_product_price ON prices (product_id, price);
这个索引能让数据库直接通过索引定位到符合条件的价格记录,完全避免对Price表的全表扫描,对性能提升非常明显。
你提到“无法直接替换为原生JOIN”,其实Laravel的查询构造器完全支持在链式查询中用JOIN,而且不会破坏分页和关联预加载的逻辑:
$q = Product::query(); $q->where('account_id', Auth::user()->account_id); // 保留原有代码的其他过滤逻辑 if ($request->code) { $q->where('code', 'LIKE' , "%{$request->code}%"); } if ($request->title) { $q->where('title', 'LIKE' , "%{$request->title}%"); } // 替换whereHas为JOIN if ($request->price){ $q->join('prices', 'products.id', '=', 'prices.product_id') ->where('prices.price', $request->price) ->select('products.*'); // 必须指定只查products表的字段,避免字段冲突 } $products = $q->with('price', 'images') ->orderBy('id','desc')->paginate($limit);
JOIN会把Product和Price表直接做关联查询,相比whereHas的子查询,数据库的执行计划会更高效,而且select('products.*')能保证返回的还是Product模型实例,不影响后续的with预加载。
如果不想改JOIN,也可以把whereHas换成whereExists,它生成的EXISTS子查询比whereHas默认的IN子查询性能更好——因为EXISTS只要找到匹配的记录就会停止查找,不用遍历所有结果:
if ($request->price){ $q->whereExists(function($query) use ($request){ $query->select(DB::raw(1)) ->from('prices') ->whereColumn('prices.product_id', 'products.id') ->where('prices.price', $request->price); }); }
这个写法和你原来的whereHas逻辑几乎一致,但生成的SQL更高效,完全兼容你的链式查询和分页。
如果你的Laravel版本在8.x及以上,可以用withExists来预加载关联存在的判断,再结合过滤,代码更简洁:
if ($request->price){ $q->withExists(['price' => function($query) use ($request) { $query->where('price', $request->price); }])->where('price_exists', true); }
withExists会自动给查询结果添加一个price_exists的布尔字段,我们直接过滤这个字段就行,底层同样是EXISTS子查询,性能和方案三相当。
内容的提问来源于stack exchange,提问作者Skeletor

