MySQL-在select中包含很多case语句的查询非常慢

时间:2018-10-03 10:59:37

标签: mysql case

我有一个查询,在选择中包含很多case语句,看起来像这样...

SELECT 
    ('2017-41') AS yearweek_week_1, 
    ('09/10/2017') AS date_week_1, 
    COUNT(CASE WHEN `created_at` > DATE_SUB('2017-10-02 00:00:00', INTERVAL 1 DAY) AND `created_at` < DATE_SUB('2017-10-09 00:00:00', INTERVAL 1 DAY) THEN my_user_id ELSE NULL END) AS total_week_1, 
    COUNT(DISTINCT CASE WHEN `created_at` > DATE_SUB('2017-10-02 00:00:00', INTERVAL 1 DAY) AND `created_at` < DATE_SUB('2017-10-09 00:00:00', INTERVAL 1 DAY) THEN my_user_id ELSE NULL END) AS distinct_week_1,
    ...
    // all weeks between 1 and 52 between here...
    ('2018-40') AS yearweek_week_52, 
    ('01/10/2018') AS date_week_52, 
    COUNT(CASE WHEN `created_at` > DATE_SUB('2018-09-24 00:00:00', INTERVAL 1 DAY) AND `created_at` < DATE_SUB('2018-10-01 00:00:00', INTERVAL 1 DAY) THEN my_user_id ELSE NULL END) AS total_week_52, 
    COUNT(DISTINCT CASE WHEN `created_at` > DATE_SUB('2018-09-24 00:00:00', INTERVAL 1 DAY) AND `created_at` < DATE_SUB('2018-10-01 00:00:00', INTERVAL 1 DAY) THEN my_user_id ELSE NULL END) AS distinct_week_52
FROM my_table
WHERE my_group_id = 123123
AND created_at > '2017-10-02 00:00:00'

如果有帮助,请在表创建语法:

CREATE TABLE `my_table` (
  `my_table_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `my_user_id` int(11) DEFAULT NULL,
  `my_group_id` int(11) DEFAULT NULL,
  `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`my_login_id`),
  KEY `my_user_id` (`my_user_id`),
  KEY `my_group_id` (`my_group_id`),
  KEY `created_at` (`created_at`),
  KEY `my_group_id_2` (`my_group_id`,`created_at`),
  KEY `my_user_id_2` (`my_user_id`,`created_at`),
  CONSTRAINT `fk_groups` FOREIGN KEY (`my_group_id`) REFERENCES `groups` (`group_id`) ON DELETE CASCADE ON UPDATE CASCADE,
  CONSTRAINT `fk_users` FOREIGN KEY (`my_user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

所以select中有104个case语句,这使它非常慢。

是否有更好的方式编写此查询或优化表?

使用EXPLAIN会得到以下结果

id  select_type table     type  possible_keys                          key 
key_len ref rows    Extra
1   SIMPLE      my_table  ref   my_group_id,created_at,my_group_id_2    my_group_id_2  5  const  1789636  Using index condition

2 个答案:

答案 0 :(得分:0)

这可能有点麻烦,但请耐心等待。当在查询中内联使用@变量时,MySQL具有一些强大的功能。首先是查询,然后我将描述其组成部分。

select 
      @WeekFrom := date_add( @WeekFrom, interval 7 day ) WeekBeginning,
      date_add( @WeekFrom, interval 7 day ) WeekEnding,
      concat( @FiscalYr, '-',  @WeekNum ) AS ShowDate,
      @FiscalYr := @FiscalYr + case when @WeekNum = 52 then 1 else 0 end as NextFYCheck,
      @WeekNum := case when @WeekNum = 52 then 1 else @WeekNum +1 end as PrepNextWeekCycle
   from
      my_table MT,
      ( select 
              @WeekFrom := date_sub( '2017-10-01', interval 7 day ), 
              @WeekNum := 41,
              @FiscalYr := 2017 ) sqlvars
     limit 52

您可以将其视为查询中的嵌入式程序。您可以使用:=赋值来设置值,并将结果存储为列名,以便再次用作要处理的下一条记录的新值。

接下来,执行此操作后,我不会对您的所有52周年份进行硬编码,因为下周周期需要更改的总PITA值,并担心代表所讨论的一周期间的开始/结束日期。为了解决这个问题,我编写了一个预查询,该查询使用这两个组件来构建会计年度。

打破这个。我从查询“ my_Table”开始,将一个会计年度的52条记录限制为52周。

接下来,我声明一个子选择来声明要使用的@MySQL变量。我从要代表的实际一周开始(通过date_sub)之前1周。因此,如果您的会计年度从2017年10月1日开始,那是在报价内,我减去7天。接下来,我硬性设定您的会计周数字(第41周),然后将会计年度设置为2017。

现在是字段。 @WeekFrom将代表一周的开始,因此取任何最后的值,并添加7天,以标识给定财政周的一周的开始。现在,我可以获取该日期结果,并添加7天以标识比该日期少的一周结束(稍后说明)。然后建立日期以显示为FiscalYear-WeekNumber格式。完成所有操作后,现在我可以在处理的下一个记录上查看我是否要进入下一会计年度(2018)。仅当我刚完成第52周的处理时,才能执行此操作。如果是这样,请在会计年度表示中添加1。最后,我可以在“周”编号上加1,但是如果我刚完成第52周,则将计数器设置回第1周。例如:2017-52会计年度,下一个周期为2018-1。

现在日期。由于您具有日期/时间字段,因此让我们看看日历2017年10月1日是星期日。因此,如果没有明确的时间,则假定为12:00 am。您希望该周内发生的所有交易。因此,我要添加7天,这使我们进入10月8日。但是最后一个WHERE子句(在下一个查询中)将测试10月8日之前的日期。因此,这使事情在10月7日至11:59:59 pm包括在内。因此,现在您不必担心时间部分的组成部分。

因此,现在,上面的查询(如果凭自己的优势运行)将建立并显示会计年度日历,其中包含开始/结束日期以及显示日期和实际日期顺序的显示顺序。

现在下一部分。您想要活动,请根据日期和所讨论的用户ID所在的位置执行简单的JOIN操作。以上方的整个查询为例,并在括号内的下方明确插入其中(并保留括号)

SELECT
      FYDates.ShowDate,
      FYDates.WeekBeginning,
      count(*) as Entries,
      count( distinct MT.my_user_id ) as DistinctEntries,
   from
      my_table MT
         JOIN ( TheEntireQueryAsIsFromAbove ) as FYDates
         ON MT.Created_At >= FYDates.WeekBeginning
         AND MT.Created_At < FYDates.WeekEnding
   where
      MT.my_group_id = 123123
   group by
      FYDates.ShowDate,
      FYDates.WeekBeginning
   order by
      FYDates.WeekBeginning

将JOIN通知子查询。它基于计算出的开始周的CreatedAt GREATER或EQUAL条目,并且小于ENDING DATE(因此直到11:59:59 pm)。

现在,您需要再运行一周吗?您实际上可以为

的内部默认声明进行参数化
      ( select 
              @WeekFrom := date_sub( '2017-10-01', interval 7 day ), 
              @WeekNum := 41,
              @FiscalYr := 2017 ) sqlvars

这里唯一的区别是结果将以ROWS而不是列的形式出现,但更适合于性能,可读性,未来财务周的更新等。我宁愿在单个页面上看到52行的列表,而不是宽208列,但那可能只是我:)

答案 1 :(得分:0)

我强烈建议将该查询转换为常规GROUP BY

http://sqlfiddle.com/#!9/b600c5/1

SELECT 
   DATE_FORMAT(`created_at`, '%Y-%u') AS yearweek_week,  
    COUNT(my_user_id) AS total_week, 
    COUNT(DISTINCT my_user_id) AS distinct_week
FROM my_table
WHERE my_group_id = 123123
AND created_at BETWEEN '2017-10-02 00:00:00' AND '2018-10-03 00:00:00'
GROUP BY yearweek_week

所有其余逻辑,例如查找一周中的第一天,以及如果我们错过一年中某些星期的某些数据,则可以获取所有星期,无论使用哪种语言,您都可以在后端应用程序上进行操作。

或者,即使没有数据,您也可以改进查询以将其全部获取,但这将比您的方法更具性能。

但是,即使我的方法对大数据集也不是一件好事。因此,为了改善这一点,我建议添加另一个索引列“ yearweek”,并在需要时更新值-这样我们就可以摆脱动态格式设置。只需SELECT yearweek AS yearweek_week,