【问题标题】:Optimizing Mysql Query For Group by with date functions使用日期函数优化 Mysql 查询 Group by
【发布时间】:2011-06-25 19:03:05
【问题描述】:

我有一份从汇总表中提取信息的报告,理想情况下会同时从两个期间提取信息,即当前期间和上一期间。我的表格是这样构成的:

report_table
item_id INT(11)
amount Decimal(8,2)
day DATE

主键是item_id,day。该表目前包含 37k 条记录,包含 92 个不同的项目和 1200 个不同的日期。我正在使用 Mysql 5.1。

这是我的选择语句:

SELECT r.day, sum(r.amount)/(count(distinct r.item_id)*count(r.day)) AS `current_avg_day`, 
sum(r2.amount)/(count(distinct r2.item_id)*count(r2.day)) AS `previous_avg_day` 
FROM `client_location_item` AS `cla`
 INNER JOIN `client_location` AS `cl`
 INNER JOIN `report_item_day` AS `r`
 INNER JOIN `report_item_day` AS `r2` 
 WHERE (r.item_id = cla.item_id) 
 AND (cla.location_id = cl.location_id) 
 AND (r.day between from_unixtime(1293840000) and from_unixtime(1296518399)) 
 AND (r2.day between from_unixtime(1291161600) and from_unixtime(1293839999)) 
 AND (cl.location_code = 'LOCATION')
 group by month(r.day);

目前这个查询在我的环境中需要 2.2 秒。解释计划是:

'1', 'SIMPLE', 'cl', 'ALL', 'PRIMARY', NULL, NULL, NULL, '33', 'Using where; Using temporary; Using filesort'
'1', 'SIMPLE', 'cla', 'ref', 'PRIMARY,location_id,location_id_idxfk', 'location_id', '4', 'cl.location_id', '1', 'Using index'
'1', 'SIMPLE', 'r', 'ref', 'PRIMARY', 'PRIMARY', '4', cla.asset_id', '211', 'Using where'
'1', 'SIMPLE', 'r2', 'ALL', NULL, NULL, NULL, NULL, '37602', 'Using where; Using join buffer'

如果我向“day”列添加索引,而不是让我的查询运行得更快,它会在 2.4 秒内运行。当时查询的解释计划是:

'1', 'SIMPLE', 'r2', 'range', 'report_day_day_idx', 'report_day_day_idx', '3', NULL, '1092', 'Using where; Using temporary; Using filesort'
'1', 'SIMPLE', 'r', 'range', 'PRIMARY,report_day_day_idx', 'report_day_day_idx', '3', NULL, '1180', 'Using where; Using join buffer'
'1', 'SIMPLE', 'cla', 'eq_ref', 'PRIMARY,location_id,location_id_idxfk', 'PRIMARY', '4', 'r.asset_id', '1', 'Using where'
'1', 'SIMPLE', 'cl', 'eq_ref', 'PRIMARY', 'PRIMARY', '4', cla.location_id', '1', 'Using where'

根据 MySQL 文档,最有效的 group by 执行是在有索引来检索分组列时。但它也指出,唯一可以真正利用索引的函数是 min() 和 max()。有谁知道我可以做些什么来进一步优化我的查询?或者,为什么我的“索引”版本运行速度更慢,尽管总体上的行数比非索引版本少?

创建表:

CREATE TABLE `report_item_day` (
  `item_id` int(11) NOT NULL,
  `amount` decimal(8,2) DEFAULT NULL,
  `day` date NOT NULL,
  PRIMARY KEY (`item_id`,`day`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

当然,我的另一个选择是进行 2 次 db 调用,每个时间段调用一次。如果我这样做,每个查询立即下降到 0.031 秒。我仍然觉得应该有一种方法来优化这个查询以获得可比较的结果。

【问题讨论】:

  • 您能发布 CREATE TABLE 语句吗?特别是什么引擎?
  • 发布了 CREATE TABLE 语句。
  • 我以为 PK 是默认索引的...

标签: mysql group-by query-optimization


【解决方案1】:

三件事:

1) 我在 WHERE 子句中没有看到 r2.item_id 的内容。没有它,r2 将通过笛卡尔积计算在内,并将汇总其他 item_id。

将您的原始查询更改为如下所示:

SELECT r.day
      ,sum(r.amount)/(count(distinct r.item_id)*count(r.day)) AS `current_avg_day`
      ,sum(r2.amount)/(count(distinct r2.item_id)*count(r2.day)) AS `previous_avg_day`
FROM `client_location_item` AS `cla`
INNER JOIN `client_location` AS `cl`
INNER JOIN `report_item_day` AS `r`
INNER JOIN `report_item_day` AS `r2`
WHERE (r.item_id = cla.item_id) AND (r2.item_id = cla.item_id) AND (cla.location_id = cl.location_id)
AND (r.day between from_unixtime(1293840000) and from_unixtime(1296518399))
AND (r2.day between from_unixtime(1291161600) and from_unixtime(1293839999))
AND (cl.location_code = 'LOCATION')
group by month(r.day); 

看看EXPLAIN PLAN在这之后是否会改变。

2) 这样做:ALTER TABLE report_itme_day ADD INDEX (date,item_id);

这将索引扫描日期而不是项目 ID。

看看EXPLAIN PLAN在这之后是否会改变。

3) 最后的手段:重构查询

SELECT r.day, sum(r.amount)/(count(distinct r.item_id)*count(r.day)) AS `current_avg_day`, sum(r2.amount)/(count(distinct r2.item_id)*count(r2.day)) AS `previous_avg_day` FROM
(SELECT CLA.item_id FROM client_location CL,client_location_item CLA WHERE CLA.location_code = 'LOCATION' AND CLA.location_id=CL.location_id) A,
report_item_day r,
report_item_day r2,
WHERE (r.item_id  = A.item_id)
AND   (r2.item_id = A.item_id)
AND   (r.day  between from_unixtime(1293840000) and from_unixtime(1296518399))
AND   (r2.day between from_unixtime(1291161600) and from_unixtime(1293839999))
group by month(r.day); 

这绝对可以进一步重构。我只是稍微重构了一下。

试试看!!!

【讨论】:

  • 我故意得到所有产品的总和,report_item_day 表上的主键已经是日期和 item_id。
【解决方案2】:

为什么在按月分组时选择日期?我并不完全希望您的查询输出看起来像。 我讨厌 MySQL 允许这样做!

我将向您展示两种一次性查询 2 个时段的方法。第一个是联合所有查询。它应该做你的 2-query 方法已经做的事情。它将返回 2 行,每个周期一个。

select sum(r.amount)  / (count(distinct r.item_id)  * count(r.day) ) as curr_avg
  from report_item_day r
  join client_location_item cla using(item_id)
  join client_location      cl  using(location_id)
 where cl.location_code = 'LOCATION'
   and r.day between from_unixtime(1293840000) and from_unixtime(1296518399)
union all
select sum(r.amount)  / (count(distinct r.item_id)  * count(r.day) ) as prev_avg
  from report_item_day r
  join client_location_item cla using(item_id)
  join client_location      cl  using(location_id)
 where cl.location_code = 'LOCATION'
   and r.day between from_unixtime(1291161600) and from_unixtime(1293839999)

下面的方法可能比上面的方法更快,但它更丑陋且更难阅读。

select period
      ,sum(amount) / (count(distinct item_id) * count(day) ) as avg_day
  from (select case when r.day between from_unixtime(1293840000) and from_unixtime(1296518399) then 'Current'
                    when r.day between from_unixtime(1291161600) and from_unixtime(1293839999) then 'Previous'
                end as period
               ,r.amount
               ,r.item_id
               ,r.day
           from report_item_day r
           join client_location_item cla using(item_id)
           join client_location      cl  using(location_id)
          where cl.location_code = 'LOCATION'
            and (    r.day between from_unixtime(1293840000) and from_unixtime(1296518399)
                  or r.day between from_unixtime(1291161600) and from_unixtime(1293839999)
                )
         ) v
 group 
     by period;

注1:你没有给我们DDL,所以我无法测试语法是否正确
注意 2:考虑创建一个以 DATE 为键的日历表。添加适当的列,例如 MONTH、WEEK、FINANCIAL_YEAR 等,以便能够支持您正在执行的报告。查询将更容易编写和理解。

【讨论】:

  • 第二种方法比我预期的要快得多。谢谢!
【解决方案3】:

首先(这可能只是美学),为什么不在 INNER JOIN 中使用 ON / USING 子句?为什么在 FROM 中的 WHERE 子句而不是实际部分上进行 JOIN?

其次,我对索引与非索引问题的猜测是,现在它必须首先检查索引以查找与所述范围匹配的记录,而在非索引版本中,内存比磁盘快。但我不能太确定。

现在,查询。这是文档的一部分。关于 JOIN:

The `conditional_expr` used with ON is any conditional expression of the form 
that can be used in a WHERE clause. Generally, you should use the ON clause for
conditions that specify how to join tables, and the WHERE clause to restrict
which rows you want in the result set.

是的,将连接条件移至 FROM 子句。此外,您可能对索引提示语法感兴趣:http://dev.mysql.com/doc/refman/5.0/en/index-hints.html

最后,您可以尝试使用视图,但要注意性能问题:http://www.mysqlperformanceblog.com/2007/08/12/mysql-view-as-performance-troublemaker/

祝你好运。

【讨论】:

  • 更新我的查询以使用 ON 进行连接并没有导致任何明显的性能差异,而且它根本没有改变解释计划。由于 MySQL 在使用聚合函数时处理视图的方式,视图不太可能是解决这个特定问题的好方法。
  • 那我猜你可能遇到了障碍。我猜测了您的索引版本较慢的原因,但据我所知,您的查询已针对我的知识范围进行了足够优化。
猜你喜欢
  • 1970-01-01
  • 2011-05-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-16
  • 2019-08-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多