即使覆盖索引,也优化COUNT(DISTINCT)的缓慢

时间:2015-03-16 12:20:17

标签: mysql sql aggregate-functions query-performance mysql-variables

我们在MySql中有一个包含大约3000万条记录的表,以下是表结构

CREATE TABLE `campaign_logs` (
  `domain` varchar(50) DEFAULT NULL,
  `campaign_id` varchar(50) DEFAULT NULL,
  `subscriber_id` varchar(50) DEFAULT NULL,
  `message` varchar(21000) DEFAULT NULL,
  `log_time` datetime DEFAULT NULL,
  `log_type` varchar(50) DEFAULT NULL,
  `level` varchar(50) DEFAULT NULL,
  `campaign_name` varchar(500) DEFAULT NULL,
  KEY `subscriber_id_index` (`subscriber_id`),
  KEY `log_type_index` (`log_type`),
  KEY `log_time_index` (`log_time`),
  KEY `campid_domain_logtype_logtime_subid_index` (`campaign_id`,`domain`,`log_type`,`log_time`,`subscriber_id`),
  KEY `domain_logtype_logtime_index` (`domain`,`log_type`,`log_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |

以下是我的查询

我正在做UNION ALL而不是使用IN操作

SELECT log_type,
       DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
       count(DISTINCT subscriber_id) AS COUNT,
       COUNT(subscriber_id) AS total
FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index)
WHERE DOMAIN='xxx'
  AND campaign_id='123'
  AND log_type = 'EMAIL_OPENED'
  AND log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+00:00','+05:30') AND CONVERT_TZ('2015-03-01 23:59:58','+00:00','+05:30')
GROUP BY log_date

UNION ALL

SELECT log_type,
       DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
       COUNT(DISTINCT subscriber_id) AS COUNT,
            COUNT(subscriber_id) AS total
FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index)
WHERE DOMAIN='xxx'
  AND campaign_id='123'
  AND log_type = 'EMAIL_SENT'
  AND log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+00:00','+05:30') AND CONVERT_TZ('2015-03-01 23:59:58','+00:00','+05:30')
GROUP BY log_date

UNION ALL

SELECT log_type,
       DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
       COUNT(DISTINCT subscriber_id) AS COUNT,
            COUNT(subscriber_id) AS total
FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index)
WHERE DOMAIN='xxx'
  AND campaign_id='123'
  AND log_type = 'EMAIL_CLICKED'
  AND log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+00:00','+05:30') AND CONVERT_TZ('2015-03-01 23:59:58','+00:00','+05:30')
GROUP BY log_date,

以下是我的解释声明

+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+------------------------------------------+
| id | select_type  | table         | type  | possible_keys                             | key                                       | key_len | ref  | rows   | Extra                                    |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+------------------------------------------+
|  1 | PRIMARY      | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468     | NULL |  55074 | Using where; Using index; Using filesort |
|  2 | UNION        | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468     | NULL | 330578 | Using where; Using index; Using filesort |
|  3 | UNION        | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468     | NULL |   1589 | Using where; Using index; Using filesort |
| NULL | UNION RESULT | <union1,2,3>  | ALL   | NULL                                      | NULL                                      | NULL    | NULL |   NULL |                                          |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+------------------------------------------+
  1. 我将COUNT(subscriber_id)更改为COUNT(*),但未发现任何性能提升。
  2. 2.我从查询中删除了COUNT(DISTINCT subscriber_id),然后我变得庞大   性能提升,我以前在大约1.5秒内得到结果   花了50秒--1分钟。但我需要从查询

    中统一计数subscriber_id

    当我从查询

    中删除COUNT(DISTINCT subscriber_id)时,会解释以下内容
    +----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+-----------------------------------------------------------+
    | id | select_type  | table         | type  | possible_keys                             | key                                       | key_len | ref  | rows   | Extra                                                     |
    +----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+-----------------------------------------------------------+
    |  1 | PRIMARY      | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468     | NULL |  55074 | Using where; Using index; Using temporary; Using filesort |
    |  2 | UNION        | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468     | NULL | 330578 | Using where; Using index; Using temporary; Using filesort |
    |  3 | UNION        | campaign_logs | range | campid_domain_logtype_logtime_subid_index | campid_domain_logtype_logtime_subid_index | 468     | NULL |   1589 | Using where; Using index; Using temporary; Using filesort |
    | NULL | UNION RESULT | <union1,2,3>  | ALL   | NULL                                      | NULL                                      | NULL    | NULL |   NULL |                                                           |
    +----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+-----------------------------------------------------------+
    
    1. 我通过删除UNION ALL单独运行了三个查询。 ONe查询需要32秒,其他查询需要1.5秒,但是第一个查询处理大约350K记录而其他查询只处理2k行
    2. 我可以通过省略COUNT(DISTINCT...)来解决我的性能问题,但我需要这些值。有没有办法重构我的查询,或添加索引或其他东西,以获得COUNT(DISTINCT...)值,但更快?

      更新 以下信息是关于上表的数据分布

      有 1个域名 1个活动 20 log_types 1k-200k用户

      以上查询我正在运行,该域名拥有180k +订阅者。

6 个答案:

答案 0 :(得分:5)

如果没有count(distinct)的查询速度更快,也许您可​​以进行嵌套聚合:

SELECT log_type, log_date,
       count(*) AS COUNT, sum(cnt) AS total
FROM (SELECT log_type,
             DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
             subscriber_id, count(*) as cnt
      FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index)
      WHERE DOMAIN = 'xxx' AND
            campaign_id = '123' AND
            log_type IN ('EMAIL_SENT', 'EMAIL_OPENED', 'EMAIL_CLICKED') AND
            log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+00:00','+05:30') AND 
                             CONVERT_TZ('2015-03-01 23:59:58','+00:00','+05:30')
      GROUP BY logtype, log_date, subscriber_id
     ) l
GROUP BY logtype, log_date;

运气好的话,这需要2-3秒而不是50秒。但是,您可能需要将其分解为子查询,以获得完整的性能。因此,如果这没有显着的性能提升,请将in更改回=其中一种类型。如果可以,那么union all可能是必要的。

编辑:

另一种尝试是使用变量来枚举group by

之前的值
SELECT log_type, log_date, count(*) as cnt,
       SUM(rn = 1) as sub_cnt
FROM (SELECT log_type,
             DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
             subscriber_id,
             (@rn := if(@clt = concat_ws(':', campaign_id, log_type, log_time), @rn + 1,
                        if(@clt := concat_ws(':', campaign_id, log_type, log_time), 1, 1)
                       )
              ) as rn
      FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index) CROSS JOIN
           (select @rn := 0)
      WHERE DOMAIN = 'xxx' AND
            campaign_id = '123' AND
            log_type IN ('EMAIL_SENT', 'EMAIL_OPENED', 'EMAIL_CLICKED') AND
            log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00', '+00:00', '+05:30') AND 
                             CONVERT_TZ('2015-03-01 23:59:58', '+00:00', '+05:30')
      ORDER BY logtype, log_date, subscriber_id
     ) t
GROUP BY log_type, log_date;

这仍然需要另一种数据,但它可能有所帮助。

答案 1 :(得分:3)

回答你的问题:

  

有没有办法重构我的查询,或添加索引或其他东西   获取COUNT(DISTINCT ...)值,但更快?

是,不要按计算字段分组(不要按功能结果分组)。相反,预先计算它,将其保存到持久列并将此持久列包含在索引中。

我会尝试执行以下操作,看看它是否会显着改变性能。

1)简化查询并专注于一个部分。 在三个中只留下一个最长的SELECT,在调整期间摆脱UNION。优化最长SELECT后,添加更多内容并检查完整查询的工作方式。

2)通过函数的结果进行分组不会让引擎有效地使用索引。 使用此函数的结果向表中添加另一列(首先暂时,只是为了检查想法)。据我所知,您想要分组1小时,因此请添加列log_time_hour datetime并将其设置为log_time舍入/截断到最接近的小时(保留日期组件)。

使用新列添加索引:(domain, campaign_id, log_type, log_time_hour, subscriber_id)。索引中前三列的顺序无关紧要(因为您使用等式与查询中的某个常量进行比较,而不是范围),但要使它们与查询中的顺序相同。或者,更好的是,按照选择性的顺序在索引定义和查询中创建它们。如果您有100,000个广告系列,1000个域和3个日志类型,请按以下顺序排列:campaign_id, domain, log_type。这应该不重要,但值得检查。 log_time_hour必须在索引定义中排在第四位,并且subscriber_id排在最后。

在查询中使用WHEREGROUP BY中的新列。确保在GROUP BY中包含所有必需的列:log_typelog_time_hour

您是否需要COUNTCOUNT(DISTINCT)?仅保留COUNT并测量性能。仅保留COUNT(DISTINCT)并衡量效果。保留两者并测量性能。看看他们如何比较。

SELECT log_type,
       log_time_hour,
       count(DISTINCT subscriber_id) AS distinct_total,
       COUNT(subscriber_id) AS total
FROM stats.campaign_logs
WHERE DOMAIN='xxx'
  AND campaign_id='123'
  AND log_type = 'EMAIL_OPENED'
  AND log_time_hour >= '2015-02-01 00:00:00' 
  AND log_time_hour <  '2015-03-02 00:00:00'
GROUP BY log_type, log_time_hour

答案 2 :(得分:1)

SELECT log_type,
       DATE_FORMAT(CONVERT_TZ(log_time,'+00:00','+05:30'),'%l %p') AS log_date,
       count(DISTINCT subscriber_id) AS COUNT,
       COUNT(subscriber_id) AS total
FROM stats.campaign_logs USE INDEX(campid_domain_logtype_logtime_subid_index)
WHERE DOMAIN='xxx'
  AND campaign_id='123'
  AND log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+00:00','+05:30') AND CONVERT_TZ('2015-03-01 23:59:58','+00:00','+05:30')
GROUP BY log_type, log_date

如果需要,请添加AND log_type IN ('EMAIL_OPENED', 'EMAIL_SENT', 'EMAIL_CLICKED')

答案 3 :(得分:1)

我会尝试使用你正在使用的索引的其他排序,移动subscriber_id,看看效果是什么。通过以更高的基数移动列,您可以获得更好的结果。

起初,我认为它可能只是使用索引的一部分(根本没有到达subscriber_id)。如果它不能使用subscriber_id,那么将其向上移动将导致它运行得更慢,这至少会告诉你它不能使用它。

我无法想到你可以玩的其他东西。

答案 4 :(得分:1)

  1. subscriber_id在您的密钥中没用,因为您在计算不同的订阅者之前按密钥(log_date)之外的计算字段进行分组。它解释了为什么这么慢,因为MySQL必须在不使用密钥的情况下对重复的订阅者进行排序和过滤。

  2. 您的log_time条件可能存在错误:您应该对select(即'+05:30','+00:00')进行相反的时区转换,但它不会对您的查询时间产生任何重大影响。

  3. 你可以避免&#34;结合所有&#34;通过log_type IN (...)

  4. 执行log_type, log_date和分组

    最有效的解决方案是在数据库模式中添加一个中午字段,并在当天的48小时中设置其中一个(并处理中午时区)。因此,您可以在campaign_iddomainlog_typelog_mid_hoursubscriber_id

    上使用索引

    这将是多余的,但会提高速度。

    所以这应该导致你的表中的一些初始化: 小心:不要在生产表上测试

    ALTER TABLE campaign_logs
       ADD COLUMN log_mid_hour TINYINT AFTER log_time;
    
    UPDATE campaign_logs SET log_mid_hour=2*HOUR(log_time)+IF(MINUTE(log_time)>29,1,0);
    
    ALTER TABLE campaign_logs
    ADD INDEX(`campaign_id`,`domain`,`log_time`,`log_type`,`log_mid_hour`,`subscriber_id`);
    

    您还必须在脚本中设置log_mid_hour以备将来使用。

    您的查询将变为(11小时中班时间)

    SELECT log_type,
       MOD(log_mid_hour+11, 48) tz_log_mid_hour,
       COUNT(DISTINCT subscriber_id) AS COUNT,
       COUNT(subscriber_id) AS total
    FROM stats.campaign_logs
    WHERE DOMAIN='xxx'
       AND campaign_id='123'
       AND log_type IN('EMAIL_SENT', 'EMAIL_OPENED','EMAIL_CLICKED')
       AND log_time BETWEEN CONVERT_TZ('2015-02-01 00:00:00','+05:30','+00:00')   
       AND CONVERT_TZ('2015-03-01 23:59:58','+05:30','+00:00')
    GROUP BY log_type, log_mid_hour;
    

    这将为您提供每个中午时段的计数,以充分利用您的索引。

答案 5 :(得分:0)

我有一个非常类似的问题,发布在这里,并得到了一些很好的帮助。这是主题:MySQL MyISAM slow count() query despite covering index

简而言之,我发现我的问题与查询或索引没有任何关系,而且一切都与我设置表和MySQL的方式有关。当我:

时,我完全相同的查询变得更快
  1. 切换到InnoDB(您已经使用过)
  2. 将CHARSET切换为ASCII。如果您不需要utf8,则需要3倍的空间(以及搜索时间)。
  3. 使每个列的大小尽可能小,如果可能的话,不要为空。
  4. 增加了MySQL的InnoDB缓冲池大小。如果这是一台专用机器,许多建议是将其增加到RAM的70%。
  5. 我通过覆盖索引对表进行排序,通过SELECT INTO OUTFILE写出来,然后将其重新插入到新表中。这会对搜索顺序中的所有记录进行物理排序。
  6. 我不知道哪些更改解决了我的问题(因为我不科学,并且一次不尝试一次),但它使我的查询速度提高了50-100倍。 YMMV。