我们在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 | |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+------------------------------------------+
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 | |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+-----------------------------------------------------------+
我可以通过省略COUNT(DISTINCT...)
来解决我的性能问题,但我需要这些值。有没有办法重构我的查询,或添加索引或其他东西,以获得COUNT(DISTINCT...)
值,但更快?
更新 以下信息是关于上表的数据分布
有 1个域名 1个活动 20 log_types 1k-200k用户
以上查询我正在运行,该域名拥有180k +订阅者。
答案 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
排在最后。
在查询中使用WHERE
和GROUP BY
中的新列。确保在GROUP BY
中包含所有必需的列:log_type
和log_time_hour
。
您是否需要COUNT
和COUNT(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)
subscriber_id
在您的密钥中没用,因为您在计算不同的订阅者之前按密钥(log_date)之外的计算字段进行分组。它解释了为什么这么慢,因为MySQL必须在不使用密钥的情况下对重复的订阅者进行排序和过滤。
您的log_time条件可能存在错误:您应该对select(即'+05:30','+00:00'
)进行相反的时区转换,但它不会对您的查询时间产生任何重大影响。
你可以避免&#34;结合所有&#34;通过log_type IN (...)
log_type, log_date
和分组
醇>
最有效的解决方案是在数据库模式中添加一个中午字段,并在当天的48小时中设置其中一个(并处理中午时区)。因此,您可以在campaign_id
,domain
,log_type
,log_mid_hour
,subscriber_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的方式有关。当我:
时,我完全相同的查询变得更快我不知道哪些更改解决了我的问题(因为我不科学,并且一次不尝试一次),但它使我的查询速度提高了50-100倍。 YMMV。