Laravel Eloquent慢查询排查:MySQL计数查询耗时过长问题
排查Laravel Eloquent生成的慢MySQL查询方案
首先,咱们先拆解下这条慢查询的核心逻辑:统计custom_products中未隐藏(hidden=0)、属于指定公司(company_id=1)、且关联了未软删除分类的产品总数,还有另一个未写完的exists子查询(推测是关联其他关联表)。这类查询变慢通常和索引缺失、查询结构不合理或数据类型不匹配有关,咱们一步步来排查优化:
1. 优先检查索引(最常见的慢查询原因)
索引是提升查询速度的关键,针对这条查询,你需要确保以下索引存在:
custom_products表:创建复合索引(hidden, company_id, id)。主查询的过滤条件是hidden和company_id,加上id可以让MySQL直接通过索引获取需要关联的ID,无需回表查询整行数据(覆盖索引优化)。CREATE INDEX idx_custom_products_hidden_company_id ON custom_products(hidden, company_id, id);categorizables表:创建复合索引(categorizable_type, categorizable_id, category_id)。子查询里需要过滤指定模型类型(categorizable_type),关联产品ID(categorizable_id),还要关联分类ID(category_id),这个复合索引能快速定位到符合条件的关联记录。CREATE INDEX idx_categorizables_type_id_category ON categorizables(categorizable_type, categorizable_id, category_id);categories表:确保主键id有索引(默认主键自带),如果categories表数据量很大,建议给deleted_at加个单独索引,因为子查询里过滤了deleted_at is null:CREATE INDEX idx_categories_deleted_at ON categories(deleted_at);- 注意:如果第二个
exists子查询关联了其他表(比如inventory之类的),同样要给对应关联字段加合适的复合索引。
2. 修正数据类型匹配问题
看你的查询里用了hidden = '0',如果hidden字段是tinyint/boolean类型,用字符串'0'会触发MySQL的隐式类型转换,导致索引失效。在Laravel里要改成:
->where('hidden', 0) // 或者 ->where('hidden', false)
确保查询条件的类型和字段类型一致,避免索引无法被利用。
3. 优化查询结构:用JOIN替代EXISTS(视场景选择)
EXISTS在判断存在性时效率不错,但如果关联表的索引不够理想,换成INNER JOIN配合DISTINCT可能更高效(因为COUNT(*)会统计重复行,所以要加DISTINCT去重)。对应的Laravel写法:
$count = CustomProduct::where('hidden', 0) ->where('company_id', 1) ->join('categorizables', function ($join) { $join->on('custom_products.id', '=', 'categorizables.categorizable_id') ->where('categorizables.categorizable_type', 'App\Models\CustomProduct'); }) ->join('categories', 'categories.id', '=', 'categorizables.category_id') ->whereNull('categories.deleted_at') // 这里加上第二个exists对应的join逻辑 ->distinct() ->count('custom_products.id');
可以对比两种写法的执行时间,选择更高效的那个。
4. 用EXPLAIN分析执行计划
直接在MySQL里运行EXPLAIN命令,查看查询的执行细节:
EXPLAIN select count(*) as aggregate from `custom_products` where `hidden` = '0' and (`company_id` = '1') and exists ( select * from `categories` inner join `categorizables` on `categories`.`id` = `categorizables`.`category_id` where `custom_products`.`id` = `categorizables`.`categorizable_id` and `categorizables`.`categorizable_type` = 'App\Models\CustomProduct' and `categories`.`deleted_at` is null);
重点看这几个字段:
type:如果出现ALL(全表扫描),说明对应的表没有用到索引,需要补建索引;key:显示实际用到的索引,确认是否和你预期的一致;rows:显示MySQL预估要扫描的行数,如果这个数字远大于实际符合条件的行数,说明索引或查询条件有问题。
5. 缓存优化(非实时场景)
如果这个统计数据不需要实时更新,可以用Laravel的缓存功能把结果缓存起来,比如缓存5分钟:
$count = Cache::remember('custom_product_count_1', 300, function () { return CustomProduct::where('hidden', 0) ->where('company_id', 1) ->whereHas('categories', function ($query) { $query->whereNull('deleted_at'); }) // 加上第二个关联条件 ->count(); });
这样可以大幅减少数据库的查询压力。
内容的提问来源于stack exchange,提问作者Rafael D'Arrigo
相关产品推荐
相关产品推荐

