即使使用覆盖索引也能优化 COUNT(DISTINCT) 缓慢
Optimizing COUNT(DISTINCT) slowness even with covering indexes
我们在MySql中有一个table,大约有3000万条记录,下面是table结构
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 | |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+------------------------------------------+
- 我将 COUNT(subscriber_id) 更改为 COUNT(*),但没有发现性能提升。
2.I 从查询中删除了 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 | |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+-----------------------------------------------------------+
- 我 运行 通过删除 UNION ALL 分别进行三个查询。一个查询用了 32 秒,其他每个查询用了 1.5 秒,但第一个查询处理大约 350K 条记录,而其他查询只处理 2k 行
我可以通过省略 COUNT(DISTINCT...)
来解决我的性能问题,但我需要这些值。有没有一种方法可以重构我的查询,或者添加索引或其他东西来获取 COUNT(DISTINCT...)
值,但速度要快得多?
更新
以下信息是以上table
的数据分布
为
1 个域名
1 个活动
20 log_types
1k-200k 订阅者
上面的查询我是 运行 的,域拥有 180k+ 订阅者。
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')
。
我会尝试对您正在使用的索引进行其他排序,移动 subscriber_id,看看效果如何。通过将具有更高基数的列向上移动,您可能会获得更好的结果。
起初,我认为它可能只使用了部分索引(根本没有达到 subscriber_id)。如果它不能使用 subscriber_id,那么将它向上移动索引树会导致它 运行 变慢,这至少会告诉你它不能使用它。
我想不出还有什么可以玩的了。
回答你的问题:
Is there a way to refactor my query, or add an index, or something, to
get the COUNT(DISTINCT...) values, but much faster?
是的,不按计算字段分组(不按函数的结果分组)。相反,预先计算它,将它保存到持久列并将这个持久列包含到索引中。
我会尝试执行以下操作,看看它是否会显着改变性能。
1) 简化查询,专注于一个部分。
三个中只留下一个最长的运行 SELECT
,去掉UNION
作为调整周期。一旦最长的 SELECT
被优化,添加更多并检查完整查询的工作方式。
2) 按函数结果分组不会让引擎有效地使用索引。
使用此函数的结果将另一列添加到 table(起初是暂时的,只是为了检查想法)。据我所知,您想按 1 小时分组,因此添加列 log_time_hour datetime
并将其设置为 log_time
rounded/truncated 到最接近的小时(保留日期部分)。
使用新列添加索引:(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
subscriber_id
在您的密钥中没有用,因为您在计算不同订阅者之前按密钥 (log_date) 外的计算字段进行分组。它解释了为什么这么慢,因为 MySQL 必须在不使用密钥的情况下对重复的订阅者进行排序和过滤。
您的 log_time 条件可能有误:您应该进行与 select 相反的时区转换(即 '+05:30','+00:00'
),但它不会对您的查询时间有任何重大影响。
您可以通过执行 log_type IN (...)
并按 log_type, log_date
分组来避免 "union all"
最有效的解决方案是在您的数据库模式中添加一个中点字段,并在其中设置一天中的 48 个中点之一(并注意中点时区)。因此,您可以在 campaign_id
、domain
、log_type
、log_mid_hour
、subscriber_id
上使用索引
这将是相当多余的,但会提高速度。
所以这应该会导致在您的 table 中进行一些初始化:
小心:不要在你的产品中测试这个 table
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;
这将为您提供每个中午的计数,充分利用您的索引。
如果没有 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 log_type, 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 log_type, log_date, subscriber_id
) t
GROUP BY log_type, log_date;
这仍然需要另一种数据,但它可能会有所帮助。
我有一个非常相似的问题,在 SO 上发布,并得到了很大的帮助。这是主题:
简而言之,我发现我的问题与查询或索引无关,而与我设置 table 和 MySQL 的方式有关。当我执行以下操作时,我完全相同的查询变得更快:
- 切换到 InnoDB(您已经在使用)
- 将 CHARSET 切换为 ASCII。如果您不需要 utf8,则需要 3 倍的时间 space(以及搜索时间)。
- 使每个列的大小尽可能小,尽可能不为空。
- 增加了 MySQL 的 InnoDB 缓冲池大小。如果这是一台专用机器,许多建议是将其增加到 RAM 的 70%。
- 我按覆盖索引对 table 进行了排序,通过 SELECT 写入 OUTFILE,然后将其重新插入到新的 table 中。这会按搜索顺序对所有记录进行物理排序。
我不知道这些更改中的哪一个解决了我的问题(因为我不科学并且没有一次尝试一个),但它使我的查询速度提高了 50-100 倍。 YMMV.
我们在MySql中有一个table,大约有3000万条记录,下面是table结构
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 | |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+------------------------------------------+
- 我将 COUNT(subscriber_id) 更改为 COUNT(*),但没有发现性能提升。
2.I 从查询中删除了 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 | |
+----+--------------+---------------+-------+-------------------------------------------+-------------------------------------------+---------+------+--------+-----------------------------------------------------------+
- 我 运行 通过删除 UNION ALL 分别进行三个查询。一个查询用了 32 秒,其他每个查询用了 1.5 秒,但第一个查询处理大约 350K 条记录,而其他查询只处理 2k 行
我可以通过省略 COUNT(DISTINCT...)
来解决我的性能问题,但我需要这些值。有没有一种方法可以重构我的查询,或者添加索引或其他东西来获取 COUNT(DISTINCT...)
值,但速度要快得多?
更新 以下信息是以上table
的数据分布为 1 个域名 1 个活动 20 log_types 1k-200k 订阅者
上面的查询我是 运行 的,域拥有 180k+ 订阅者。
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')
。
我会尝试对您正在使用的索引进行其他排序,移动 subscriber_id,看看效果如何。通过将具有更高基数的列向上移动,您可能会获得更好的结果。
起初,我认为它可能只使用了部分索引(根本没有达到 subscriber_id)。如果它不能使用 subscriber_id,那么将它向上移动索引树会导致它 运行 变慢,这至少会告诉你它不能使用它。
我想不出还有什么可以玩的了。
回答你的问题:
Is there a way to refactor my query, or add an index, or something, to get the COUNT(DISTINCT...) values, but much faster?
是的,不按计算字段分组(不按函数的结果分组)。相反,预先计算它,将它保存到持久列并将这个持久列包含到索引中。
我会尝试执行以下操作,看看它是否会显着改变性能。
1) 简化查询,专注于一个部分。
三个中只留下一个最长的运行 SELECT
,去掉UNION
作为调整周期。一旦最长的 SELECT
被优化,添加更多并检查完整查询的工作方式。
2) 按函数结果分组不会让引擎有效地使用索引。
使用此函数的结果将另一列添加到 table(起初是暂时的,只是为了检查想法)。据我所知,您想按 1 小时分组,因此添加列 log_time_hour datetime
并将其设置为 log_time
rounded/truncated 到最接近的小时(保留日期部分)。
使用新列添加索引:(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
subscriber_id
在您的密钥中没有用,因为您在计算不同订阅者之前按密钥 (log_date) 外的计算字段进行分组。它解释了为什么这么慢,因为 MySQL 必须在不使用密钥的情况下对重复的订阅者进行排序和过滤。您的 log_time 条件可能有误:您应该进行与 select 相反的时区转换(即
'+05:30','+00:00'
),但它不会对您的查询时间有任何重大影响。您可以通过执行
log_type IN (...)
并按log_type, log_date
分组来避免 "union all"
最有效的解决方案是在您的数据库模式中添加一个中点字段,并在其中设置一天中的 48 个中点之一(并注意中点时区)。因此,您可以在 campaign_id
、domain
、log_type
、log_mid_hour
、subscriber_id
这将是相当多余的,但会提高速度。
所以这应该会导致在您的 table 中进行一些初始化: 小心:不要在你的产品中测试这个 table
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;
这将为您提供每个中午的计数,充分利用您的索引。
如果没有 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 log_type, 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 log_type, log_date, subscriber_id
) t
GROUP BY log_type, log_date;
这仍然需要另一种数据,但它可能会有所帮助。
我有一个非常相似的问题,在 SO 上发布,并得到了很大的帮助。这是主题:
简而言之,我发现我的问题与查询或索引无关,而与我设置 table 和 MySQL 的方式有关。当我执行以下操作时,我完全相同的查询变得更快:
- 切换到 InnoDB(您已经在使用)
- 将 CHARSET 切换为 ASCII。如果您不需要 utf8,则需要 3 倍的时间 space(以及搜索时间)。
- 使每个列的大小尽可能小,尽可能不为空。
- 增加了 MySQL 的 InnoDB 缓冲池大小。如果这是一台专用机器,许多建议是将其增加到 RAM 的 70%。
- 我按覆盖索引对 table 进行了排序,通过 SELECT 写入 OUTFILE,然后将其重新插入到新的 table 中。这会按搜索顺序对所有记录进行物理排序。
我不知道这些更改中的哪一个解决了我的问题(因为我不科学并且没有一次尝试一个),但它使我的查询速度提高了 50-100 倍。 YMMV.