【问题标题】:How to optimize query with many joins?如何优化具有多个连接的查询?
【发布时间】:2016-12-19 19:07:37
【问题描述】:

我有一个简单但很长的查询,它计算结果的内容大约需要 14 秒。主表上的计数本身花费不到一秒钟,但在多次加入后延迟太高,如下所示

Select  Count(Distinct visits.id) As Count_id
    From  visits
    Left Join  clients_locations  ON visits.client_location_id = clients_locations.id
    Left Join  clients  ON clients_locations.client_id = clients.id
    Left Join  locations  ON clients_locations.location_id = locations.id
    Left Join  users  ON visits.user_id = users.id
    Left Join  potentialities  ON clients_locations.potentiality = potentialities.id
    Left Join  classes  ON clients_locations.class = classes.id
    Left Join  professions  ON clients.profession_id = professions.id
    Inner Join  specialties  ON clients.specialty_id = specialties.id
    Left Join  districts  ON locations.district_id = districts.id
    Left Join  provinces  ON districts.province_id = provinces.id
    Left Join  locations_types  ON locations.location_type_id = locations_types.id
    Left Join  areas  ON clients_locations.area_id = areas.id
    Left Join  calls  ON calls.visit_id = visits.id 

解释的输出是

+---+---+---+---+---+---+---+---+---+---+
|编号 |选择类型 |表|类型 |可能的键 |关键 | key_len |参考 |行 |额外 |
+---+---+---+---+---+---+---+---+---+---+
| 1 |简单 |特产 |索引 |初级 |专业名称 | 52 |空 | 53 |使用索引 |
| 1 |简单 |客户 |参考 |初级,专业 |专业 | 4 | crm_db.specialties.id | 143 | |
| 1 |简单 |客户位置 |参考 | PRIMARY,client_id |客户 ID | 4 | crm_db.clients.id | 1 | |
| 1 |简单 |地点 | eq_ref |初级 |初级 | 4 | crm_db.clients_locations.location_id | 1 | |
| 1 |简单 |地区 | eq_ref |初级 |初级 | 4 | crm_db.locations.district_id | 1 |使用位置 |
| 1 |简单 |访问 |参考 | unique_visit,client_location_id |独特的访问 | 4 | crm_db.clients_locations.id | 4 |使用索引 |
| 1 |简单 |来电 |参考 | call_unique,visit_id | call_unique | 4 | crm_db.visits.id | 1 |使用索引 |
+---+---+---+---+---+---+---+---+---+---+

更新 1 上面的查询与动态 where 语句 $sql = $sql . "Where ". $whereFilter 一起使用,但我以简单的形式提交了它。所以不要考虑答案只是消除连接:)

更新 2 这是动态过滤的示例

$temp = $this->province_id;
if ($temp != null) {
        $whereFilter = $whereFilter . " and provinces.id In ($temp) ";
    }

但在启动案例中,我们的案例中没有 where 语句

【问题讨论】:

  • 如果您只是计算访问中的唯一 ID,为什么还需要加入?
  • 你似乎没有过滤,所以你为什么需要count(distinct)。我的意思是,在joins 中一个或两个inner join,但似乎没有必要加入。
  • 我稍后将此查询与动态 where 语句 'Where $whereFilter' 一起使用
  • 当一个“访问”有 0 个“调用”时,是否算作 1?当“访问”有 3 个“调用”时,它算作 1 吗?还是3?这对于理解并可能消除LEFT JOINs 至关重要。
  • 如果您动态添加使用这些左连接中的字段的 where 子句,则您将它们更改为内连接。这会给你不正确的结果。

标签: mysql database query-optimization


【解决方案1】:

您似乎没有任何(或太多)有意过滤。如果你想知道calls中提到的访问次数,我建议:

select count(distinct c.visit_id)
from calls c;

【讨论】:

  • 过滤是根据动态标准完成的。我更新了问题以说明
  • 当您进行过滤时,它是否运行“足够快”?
  • 是的,使用的过滤器越多,结果越快
【解决方案2】:

左连接总是从第一个表返回一行,但如果有多个匹配行,则可能返回多行。但是因为您正在计算不同的访问行,所以在计算不同的访问时左连接到另一个表与仅计算访问的行相同。因此,影响结果的唯一联接是内部联接,因此您可以删除所有“完全”的左联接表而不影响结果。

我所说的“完全”是指一些左连接表实际上是内连接的; specialty 的内连接要求 clients 的连接成功,因此也是内连接,这反过来又要求 clients_locations 的连接成功,因此也是内连接。

您的查询(如发布的)可以简化为:

Select Count(Distinct visits.id) As Count_id
From visits
Join clients_locations ON visits.client_location_id = clients_locations.id
Join clients ON clients_locations.client_id = clients.id
Join specialties ON clients.specialty_id = specialties.id

然而,删除所有那些不必要的连接将极大地改善查询的运行时间,这不仅是因为要进行的连接更少,而且还因为当您考虑到大小是 product 所有表中的匹配项(不是 sum.

为获得最佳性能,请在所有 id-and-fk 列上创建覆盖索引:

create index visits_id_client_location_id on visits(id, client_location_id);
create index clients_locations_id_client_id on clients_locations(id, client_id);
create index clients_id_specialty_id on clients(id, specialty_id);

因此可以在可能的情况下使用仅索引扫描。我假设 PK 列上有索引。

【讨论】:

  • 来电怎么样。我应该让它与访问保持连接吗?
  • @eslam 不,不要加入calls。到另一个表的左连接将始终返回访问的每一行。如果左连接表中有多个匹配的行,它可能会返回 multiple 行,但是因为您只计算 distinct 访问 ID,所以加入多行不会给您更多不同的访问 ID。
【解决方案3】:

为了优化整个过程,您可以根据您要应用的过滤器动态构建 pre-where SQL。喜欢:

// 基础选择和左连接 $preSQL = "从访问中选择计数(不同的访问.id)作为 Count_id"; $preSQL .= "Left Join clients_locations ON visit.client_location_id = clients_locations.id"; // 按省ID过滤 $temp = $this->province_id; 如果 ($temp != null) { $preSQL .= "Left Join locations ON clients_locations.location_id = locations.id"; $preSQL .= "Left Join Districts ON locations.district_id = districts.id"; $preSQL .= "左加入省份 ON Districts.province_id = 省份.id "; $whereFilter = "provinces.id In ($temp)"; } $sql = $preSQL 。 “在哪里 ”。 $哪里过滤器; // ...

如果您使用多个过滤器,您可以将所有内部/左连接字符串放在一个数组中,然后在分析请求后,您可以使用最少的连接来构造您的 $preSQL

【讨论】:

    【解决方案4】:

    使用 COUNT(CASE WHEN visit_id!="" THEN 1 END) 作为访问。

    希望这会有所帮助

    【讨论】:

      【解决方案5】:

      不就是这样吗:

      SELECT COUNT(id)
      FROM visits
      

      因为当没有匹配的客户端时,所有左外连接也返回一个visits.id,...,调用和id 应该是唯一的?

      不同提示:一个内连接也仅在客户端存在时才有效。通常,当需要内部连接时,它们必须尽可能靠近源表,因此在您的示例中,最好放在“左连接客户端”之后的行中。

      【讨论】:

        【解决方案6】:

        我不太了解您的想法,特别是您的 INNER JOIN 会在 INNER JOIN 中转换一些 LEFT,这看起来很奇怪,但让我们尝试一个解决方案:

        通常 LEFT JOIN 的性能很差,我认为只有在 WHERE 子句中使用它们时才需要它们,然后只有在使用它们时才能将它们包含在 INNER JOIN 中。 例如:

        $query = "Select Count(Distinct visits.id) As Count_id  From  visits ";
        
        if($temp != null){
            $query .= " INNER JOIN  clients_locations  ON visits.client_location_id = clients_locations.id ";
            $query .= " INNER JOIN  locations  ON clients_locations.location_id = locations.id  ";
            $query .= " INNER JOIN  locations  ON clients_locations.location_id = locations.id ";
            $query .= " INNER JOIN  districts  ON locations.district_id = districts.id "
            $query .= " INNER JOIN  provinces  ON districts.province_id = provinces.id ";
            $whereFilter .= " and provinces.id In ($temp) ";
        }
        

        我认为它会帮助你的表现,它会根据你的需要发挥作用。

        【讨论】:

          猜你喜欢
          • 2012-06-05
          • 1970-01-01
          • 2010-10-31
          • 1970-01-01
          • 1970-01-01
          • 2020-04-30
          • 1970-01-01
          • 2019-09-03
          • 2012-02-15
          相关资源
          最近更新 更多