You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 07:14:38