Mysql Group Join 优化问题

Posted

技术标签:

【中文标题】Mysql Group Join 优化问题【英文标题】:Mysql Group Join Optimization Issue 【发布时间】:2015-03-15 21:04:31 【问题描述】:

我正在尝试优化此查询,它从 building_rent_prices 和 building_weather 返回多行,然后对它们进行分组并计算其字段的平均值。到目前为止,这些表都在一百万行以下,但需要几秒钟,有谁知道我如何从复合索引或重写查询中优化它?我假设它应该能够是 100 毫秒或更快的查询,但到目前为止它似乎不能

SELECT b.*
     , AVG(r.rent)
     , AVG(w.high_temp)
  FROM buildings b
  LEFT 
  JOIN building_rent_prices r
    ON r.building_id = b.building_id 
  LEFT 
  JOIN building_weather w
    ON w.building_id = b.building_id 
 WHERE w.date BETWEEN CURDATE() AND CURDATE + INTERVAL 4 DAY
   AND r.date BETWEEN CURDATE() AND CURDATE + INTERVAL 10 day
 GROUP  
    BY b.building_id
 ORDER  
    BY AVG(r.rent) / b.square_feet DESC
 LIMIT 10;  

解释如下:

1 SIMPLE building_rent_prices 范围

1 个简单的建筑物 eq_ref

1 个简单的建筑_天气参考

在哪里使用;使用索引;使用临时的;使用文件排序

在哪里使用

在哪里使用;使用索引

我正在处理一些测试数据,这里是创建表

CREATE TABLE building(
building_id INT PRIMARY KEY AUTO_INCREMENT, 
name VARCHAR(255),
square_feet INT
);

CREATE TABLE building_weather(
building_weather_id INT PRIMARY KEY AUTO_INCREMENT, 
building_id INT,
weather_date DATE,
high_temp INT
);

CREATE TABLE building_rates(
building_rate_id INT PRIMARY KEY AUTO_INCREMENT, 
building_id INT,
weather_date DATE,
rate double
);

ALTER TABLE building_rates INDEX(building_id);
ALTER TABLE buildings INDEX(building_id);
ALTER TABLE building_weather INDEX(building_id);

根据 DRapp 没有索引的回答,这似乎在 1 秒内工作(我仍然需要测试它是否有效)

select 
  B.*, 
  BRP.avgRent, 
  BW.avgTemp
   from 
   ( select building_id,
            AVG( rent ) avgRent
         from
            building_rent_prices
         where
            date BETWEEN CURDATE() AND CURDATE() + 10
         group by
            building_id
         order by
            building_id ) BRP
     JOIN buildings B
        on BRP.building_id = B.building_id
     left join ( select building_id,
                        AVG( hi_temp ) avgTemp
                     from building_weather 
                     where date BETWEEN CURDATE() AND CURDATE() + 10
                     group by building_id) BW
        on BRP.building_id =  BW.building_id
   GROUP BY BRP.building_id
 ORDER BY BRP.avgRent / 1 DESC
   LIMIT 10;

【问题讨论】:

EXPLAINSHOW CREATE TABLE 用于查询中的每个表。 你试过 EXPLAIN 命令了吗? 您可以将left join 更改为inner join,因为where 子句正在撤消外连接。此外,由于交叉连接效应,您的平均值可能是错误的。 谢谢,我切换到内部连接,但到目前为止性能没有变化 您至少需要 5.6 才能获得索引子查询的效率。没有它,随着建筑物数量的增加,查询会变得很慢。 【参考方案1】:

让我们详细看一下这个查询。您想要为每个建筑物报告两种不同类型的平均值。您需要在单独的子查询中计算它们。如果你不这样做,你会得到笛卡尔组合爆炸。

一个是 11 天的平均租金价格。您可以通过此子查询获取该数据:

          SELECT building_id, AVG(rent) rent
            FROM building_rent_prices
           WHERE date BETWEEN CURDATE() AND CURDATE() + INTERVAL 10 DAY
           GROUP BY building_id

这个子查询可以通过compound covering index 对building_rent_prices 进行优化,由(date, building_id, rent) 组成。

接下来是五天的平均温度。

          SELECT building_id, AVG(high_temp) high_temp
            FROM building_weather
           WHERE date BETWEEN CURDATE() AND CURDATE() + INTERVAL 4 DAY
           GROUP BY building_id

这可以通过building_weather 上的复合覆盖索引进行优化,由(date, building_id, high_temp) 组成。

最后,您需要将这两个子查询加入到您的buildings 表中以生成最终结果集。

SELECT buildings.*, a.rent, b.high_temp
  FROM buildings
  LEFT JOIN (
          SELECT building_id, AVG(rent) rent
            FROM building_rent_prices
           WHERE date BETWEEN CURDATE() AND CURDATE() + INTERVAL 10 DAY
           GROUP BY building_id
       ) AS a ON buildings.building_id = a.building_id
  LEFT JOIN (
          SELECT building_id, AVG(high_temp) high_temp
            FROM building_weather
           WHERE date BETWEEN CURDATE() AND CURDATE() + INTERVAL 4 DAY
           GROUP BY building_id
       ) AS b ON buildings.building_id = b.building_id
 ORDER BY a.rent / buildings.square_feet DESC
 LIMIT 10

一旦优化了两个子查询,除了building_id 主键之外,这个不需要任何东西。

总之,为了加快这个查询,创建building_rent_pricesbuilding_weather 查询中提到的两个复合索引。

【讨论】:

它执行得非常快,但为了获得多个结果,我必须按 building.building_id ORDER BY a.rent / building.square_feet DESC LIMIT 10 添加组,这需要 3 多秒跨度> 【参考方案2】:

不要使用 CURDATE + 4:

mysql> select CURDATE(), CURDATE() + 30, CURDATE() + INTERVAL 30 DAY;
+------------+----------------+-----------------------------+
| CURDATE()  | CURDATE() + 30 | CURDATE() + INTERVAL 30 DAY |
+------------+----------------+-----------------------------+
| 2015-03-15 |       20150345 | 2015-04-14                  |
+------------+----------------+-----------------------------+

INDEX(building_id) 添加到第二个和第三个表中。

如果这些都不能解决;回来修改查询和架构,我会更深入地研究。

【讨论】:

谢谢,如果他们看到这个,我希望对其他人有所帮助(我会纠正它以不使用那个),我在我的代码中使用了像你的示例一样的间隔,这是我在转录更简单示例时的错误 我已经在所有表上建立了 building_id 索引,它似乎与 DRapp 建议的笛卡尔相关【参考方案3】:

首先,您对基于 WEATHER 的表的查询仅为 4 天,而 RENT PRICES 表为 10 天。由于您在两者之间没有任何连接关联,因此您将得到一个笛卡尔结果,即每个建筑物 ID 有 40 条记录。这是故意的还是只是没有被识别为糟糕......

其次,我会像下面那样调整查询,而且,我已经调整了 BOTH WEATHER 和 RENT PRICES 表以反映相同的日期范围。我首先对价格进行子查询,然后按建筑物和日期分组,然后加入建筑物,然后再对按建筑物和日期分组的天气进行子查询。但是在这里,我将租金价格子查询加入到建筑物 ID 和日期的天气子查询中,因此它最多会保持 1:1 的比例。我不知道为什么天气甚至是跨越日期范围的考虑因素。

但是为了帮助索引,我建议以下内容

Table                Index on
buildings            (Building_ID)  <-- probably already exists as a PK
building_rent_prices (date, building_id, rent)
building_weather     (date, building_id, hi_temp)

索引的目的是利用 WHERE 子句(日期优先),然后是 GROUP BY(建筑物 ID),并且是一个 COVERING INDEX(包括租金)。出于同样的原因,对于建筑物天气表也是如此。

select 
      B.*, 
      BRP.avgRent, 
      BW.avgTemp
   from 
       ( select building_id,
                AVG( rent ) avgRent
             from
                building_rent_prices
             where
                date BETWEEN CURDATE() AND CURDATE() + INTERVAL 10 DAY
             group by
                building_id
             order by
                building_id ) BRP

         JOIN buildings B
            on BRP.building_id = B.building_id

         left join ( select building_id,
                            AVG( hi_temp ) avgTemp
                         from
                            building_weather 
                         where
                            date BETWEEN CURDATE() AND CURDATE() + INTERVAL 10 DAY
                         group by
                            building_id ) BW
            on BRP.building_id =  BW.building_id

   GROUP BY 
      BRP.building_id

   ORDER BY 
      BRP.avgRent / B.square_feet DESC

   LIMIT 10;

澄清...

我不能保证执行顺序,但本质上,对于 BPR 和 BW 别名的两个(查询),它们会在任何连接发生之前快速完成和执行。如果您想要(在我的示例中)10 天与每天加入的平均值,那么我已将“日期”作为组的一部分删除,因此每个最多将分别返回每个建筑物 1 个。

现在,仅以 1:1:1 的比例加入构建表将限制最终结果集中的记录。这应该考虑到您对相关日期的平均值的关注。

【讨论】:

谢谢,我有一个理论认为笛卡尔可能是一个问题,请您帮忙解释一下您的示例执行顺序吗?我相信首先将建筑价格限制在 date_range,然后加入已经加入天气限制在其日期范围内的建筑物? 如果目的是获得几天内的平均值,我是否只想按 building_id 而不是 building_id 和 date 分组? @kevinn2065,请参阅答案以获得澄清 您的解决方案运行良好,但比我在下面发布的解决方案稍慢,我发现它更难阅读,感谢您的所有帮助,它使我发现了下面的解决方案!【参考方案4】:

对于遇到与我类似的问题的任何人,解决方案是使用 building_id 对您想加入的每个表进行分组,这样您就可以与每个平均值一对一地加入。如果您不希望在所有表中都没有数据的结果,使用 JOIN 而不是 LEFT JOIN 的 Ollie Jones 查询是最接近的答案。另外我遇到的主要问题是我忘记在 avg(low_temp) 列上放置索引,因此 INDEXES.我从中学到的是,如果您在选择中执行聚合函数,则它属于您的索引。我在其中添加了 low_temp。

building_weather (date, building_id, hi_temp, low_temp) 按照 Ollie 和 DR APP 的建议

ALTER TABLE building_weather ADD index(date, building_id, hi_temp, low_temp);

SELECT buildings.*, a.rent, b.high_temp, b.low_temp
  FROM buildings
  JOIN (
      SELECT building_id, AVG(rent) rent
        FROM building_rent_prices
       WHERE date BETWEEN CURDATE() AND CURDATE() + INTERVAL 10 DAY
       GROUP BY building_id
   ) AS a ON buildings.building_id = a.building_id
  JOIN (
      SELECT building_id, AVG(high_temp) high_temp, AVG(low_temp) low_temp
        FROM building_weather
       WHERE date BETWEEN CURDATE() AND CURDATE() + INTERVAL 4 DAY
       GROUP BY building_id
   ) AS b ON buildings.building_id = b.building_id
 ORDER BY a.rent / buildings.square_feet DESC
 LIMIT 10

【讨论】:

以上是关于Mysql Group Join 优化问题的主要内容,如果未能解决你的问题,请参考以下文章

使用中间表优化 MySQL Join 和 Group By

mysql group by using filesort优化

HIVE优化场景七--数据倾斜--group by 倾斜

优化 PostgreSQL 中的 JOIN -> GROUP BY 查询:所有索引都已经存在

Day908.join&snlj&dist和group问题和备库自增主键问题 -MySQL实战

使用 GROUP BY ... HAVING 优化 MySQL 查询时遇到问题