优化SELECT计数(DISTINCT ip)

时间:2016-05-23 00:29:22

标签: mysql sql optimization distinct

我试图从每天约2M新行的表中获取汇总结果(总的唯一IP)。

表格:

CREATE TABLE `clicks` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `hash` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `type` enum('popunder','gallery','exit','direct') COLLATE utf8_unicode_ci NOT NULL,
  `impression_time` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `source_user_id` int(11) NOT NULL,
  `destination_user_id` int(11) NOT NULL,
  `destination_campaign_id` int(11) NOT NULL,
  `destination_campaign_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `destination_campaign_url` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `ip` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `referrer` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `country_code` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `country_id` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `country` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `isp` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `category_id` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `category` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `bid` float(8,2) NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`id`),
  KEY `ip` (`ip`),
  KEY `source_user_id` (`source_user_id`),
  KEY `destination_user_id` (`destination_user_id`),
  KEY `destination_campaign_id` (`destination_campaign_id`),
  KEY `clicks_hash_index` (`hash`),
  KEY `clicks_created_at_index` (`created_at`),
  KEY `campaign_date` (`destination_campaign_id`,`created_at`),
  KEY `source_user_date` (`source_user_id`,`created_at`)
) ENGINE=InnoDB AUTO_INCREMENT=301539660 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

我的查询:

SELECT SUM(ips_by_date.count) as count, ips_by_date.date as date
FROM (SELECT count(DISTINCT ip) as count, DATE(created_at) as date 
      FROM clicks as clicks 
      WHERE created_at BETWEEN '2016-05-22 00:00:00' AND '2016-05-23 23:59:59' 
      GROUP BY DATE(created_at)) as ips_by_date 
GROUP BY date;

现在,这个查询花了93秒才运行了一天,我觉得我错过了一些东西。

我是否可以进行任何优化以加快此简单计数的性能?

谢谢。

3 个答案:

答案 0 :(得分:2)

首先,我不明白为什么子查询是必要的。内部查询每个日期有一行。无需再次聚合。其次,你的查询是两天,但我得到了关于性能的观点。

所以,让我们从:

开始
SELECT count(DISTINCT ip) as count, DATE(created_at) as date 
FROM clicks  
WHERE created_at BETWEEN '2016-05-22 00:00:00' AND '2016-05-23 23:59:59' 
GROUP BY DATE(created_at);

对于此查询,您需要clicks(created_at, ip)上的索引。另请注意,我会将其写为:

SELECT count(DISTINCT ip) as count, DATE(created_at) as date 
FROM clicks  
WHERE created_at >= '2016-05-22' AND created_at < '2016-05-24' 
GROUP BY DATE(created_at);

这应该会有一些改进,但我认为它不会更好,因为外部聚合仍然需要文件排序。

答案 1 :(得分:0)

这里的性能可以归结为索引的效率,因为代码中没有太大的变化空间(请参阅Gordons代码以获得更简洁的代码版本)。

(created_at)(created_at, ip)上的索引不会直接给你distinct ip而不进一步排序(因为你不按created_at分组),但是后者至少不需要直接访问表。因此,下一次优化需要(date(created_at), ip)上的索引,即使这意味着会有一些重复的数据。

从mysql 5.7.6开始,您可以使用生成的列创建列dt as date(created_at),在5.7.6之前,只需创建一列dt并手动更新(如果您更改了您的create_at - 值,您必须添加触发器以相应地更新该列。您的初始更新可能需要一段时间,因此请批量更新或考虑将其用于将来的查询。

添加索引(dt, ip)现在应该为您提供单个索引/范围扫描并且没有filesort的结果,而无需从datetime计算date():

select count(distinct ip) as count, dt 
from clicks  
where dt >= '2016-05-22' and dt < '2016-05-24' 
group by dt;

如果一切正常,即使是几百万行也只需要几秒钟。

有些事情仍然可能会给您带来麻烦:由于90秒对于200万行来说仍然是一个相对较大的数字,因此可能表明您遇到缓冲区大小/ ram / hdd问题。如果它需要你,例如80秒将拒绝并将索引加载到内存中,之后没有多少索引可以做。一个简单的测试:运行您的查询两次。如果第二次(实际上)显着更快(例如&lt;&lt;&lt; 1/10),那么您可能不得不考虑调整系统设置,体系结构或分区。话虽如此,你不应该调整你的系统(有时甚至不添加另一个索引或日期列)来进行这样的查询,并且可能减慢其他更重要的事情 - 获取每日统计数据,你可以轻松地运行任务在午夜,你可以想到所有的统计数据,并保存结果,让你在早上好好轻松地查看,如果你的查询运行需要几个小时也没关系。

答案 2 :(得分:0)

首先添加已经提到的复合索引。然后真正的性能问题是读取数十亿行来计算COUNT(DISTINCT...)。该操作需要收集所有值,排序并执行GROUP BY,或者尝试将所有不同的值保留在RAM中。

摘要表非常适合加速数据仓库应用程序中的SUMCOUNT甚至AVG。但是COUNT(DISTINCT...)(又名&#34;统计唯一用户&#34;)不适用于摘要表。如果你愿意接受一个小错误,那就有办法了。请参阅my blog

您可能没有意识到这一点,但在VARCHAR 有时中全面使用255会导致不必要的性能问题。在这种情况下,您有ip在任何tmp表中占用765个字节,可能在相关查询中。将其更改为VARCHAR(39) CHARACTER SET ascii会将其缩短20倍! (很难预测会加快查询的速度,如果有的话。你可以通过一个简单的存储函数将其降低到BINARY(16)