使用GROUP BY的MySQL查询非常慢

时间:2018-09-02 14:08:23

标签: mysql aggregate-functions query-performance

我有一个使用以下架构的数据库:

CREATE TABLE IF NOT EXISTS `sessions` (
  `starttime` datetime NOT NULL,
  `ip` varchar(15) NOT NULL default '',
  `country_name` varchar(45) default '',
  `country_iso_code` varchar(2) default '',
  `org` varchar(128) default '',
  KEY (`ip`),
  KEY (`starttime`),
  KEY (`country_name`)
);

(实际表中包含更多列;我仅包含要查询的列。)引擎是InnoDB。

如您所见,ipstarttimecountry_name上有3个索引。

该表非常大-它包含约150万行。我正在对其进行各种查询,试图提取一个月的信息量(在下面的示例中为2018年8月)。

这样的查询

SELECT
  UNIX_TIMESTAMP(starttime) as time_sec,
  country_iso_code AS metric,
  COUNT(country_iso_code) AS value
FROM
  sessions
WHERE
  starttime >= FROM_UNIXTIME(1533070800) AND
  starttime <= FROM_UNIXTIME(1535749199)
GROUP BY metric;
尽管country_iso_code上没有索引,但

相当慢但是可以忍受(几十秒)。

(忽略SELECT中的第一件事;我知道这似乎没有意义,但是在使用查询结果的工具中是必需的。类似地,请忽略{ {1}}而不是日期字符串;查询的这一部分是自动生成的,我无法控制它。)

但是,这样的查询

FROM_UNIXTIME()

令人难以忍受的缓慢-我让它运行了大约半个小时,然后放弃了,却没有得到任何结果。

SELECT country_name AS Country, COUNT(country_name) AS Attacks FROM sessions WHERE starttime >= FROM_UNIXTIME(1533070800) AND starttime <= FROM_UNIXTIME(1535749199) GROUP BY Country; 的结果:

EXPLAIN

到底是什么问题?我应该索引其他内容吗?也许是(+----+-------------+----------+------------+-------+------------------------------------+--------------+---------+------+----------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+----------+------------+-------+------------------------------------+--------------+---------+------+----------+----------+-------------+ | 1 | SIMPLE | sessions | NULL | index | starttime,starttime_2,country_name | country_name | 138 | NULL | 14771687 | 35.81 | Using where | +----+-------------+----------+------------+-------+------------------------------------+--------------+---------+------+----------+----------+-------------+ starttime)的综合索引?我读过this guide,但也许我误会了吗?

以下是一些其他查询,它们的运行速度类似,并且可能遇到相同的问题:

查询2:

country_name

SELECT ip AS IP, COUNT(ip) AS Attacks FROM sessions WHERE starttime >= FROM_UNIXTIME(1533070800) AND starttime <= FROM_UNIXTIME(1535749199) GROUP BY ip; 的结果:

EXPLAIN

查询3:

+----+-------------+----------+------------+-------+--------------------------+------+---------+------+----------+----------+-------------+
| id | select_type | table    | partitions | type  | possible_keys            | key  | key_len | ref  | rows     | filtered | Extra       |
+----+-------------+----------+------------+-------+--------------------------+------+---------+------+----------+----------+-------------+
|  1 | SIMPLE      | sessions | NULL       | index | starttime,ip,starttime_2 | ip   | 47      | NULL | 14771780 |    35.81 | Using where |
+----+-------------+----------+------------+-------+--------------------------+------+---------+------+----------+----------+-------------+

SELECT org AS Organization, COUNT(org) AS Attacks FROM sessions WHERE starttime >= FROM_UNIXTIME(1533070800) AND starttime <= FROM_UNIXTIME(1535749199) GROUP BY Organization; 的结果:

EXPLAIN

查询4:

+----+-------------+----------+------------+-------+---------------------------+------+---------+------+----------+----------+-------------+
| id | select_type | table    | partitions | type  | possible_keys             | key  | key_len | ref  | rows     | filtered | Extra       |
+----+-------------+----------+------------+-------+---------------------------+------+---------+------+----------+----------+-------------+
|  1 | SIMPLE      | sessions | NULL       | index | starttime,starttime_2,org | org  | 387     | NULL | 14771800 |    35.81 | Using where |
+----+-------------+----------+------------+-------+---------------------------+------+---------+------+----------+----------+-------------+

SELECT ip AS IP, country_name AS Country, city_name AS City, org AS Organization, COUNT(ip) AS Attacks FROM sessions WHERE starttime >= FROM_UNIXTIME(1533070800) AND starttime <= FROM_UNIXTIME(1535749199) GROUP BY ip; 的结果:

EXPLAIN

2 个答案:

答案 0 :(得分:0)

通常,查询形式为

  SELECT column, COUNT(column)
    FROM tbl
   WHERE datestamp >= a AND datestamp <= b
   GROUP BY column
当表在(datestamp, column)上具有复合索引时,

效果最佳。为什么?可以通过索引扫描满足它们,而无需读取表的所有行。

换句话说,可以通过随机访问索引(到datestamp的第一个值)来定位查询的第一相关行。然后,MySQL可以顺序读取索引并计算column中的各个值,直到到达最后一个相关行为止。无需读取实际表;仅从索引即可满足查询。这样可以更快。

UPDATE TABLE tbl ADD INDEX date_col (datestamp, column);

为您创建索引。

当心两件事。一:单列索引不一定有助于汇总查询性能。

二:很难在不查看整个查询的情况下猜测用于索引扫描的正确索引。简化的查询通常会导致索引过度简化。

答案 1 :(得分:0)

更好...

请注意,您没有PRIMARY KEY;那很顽皮。拥有PK并不能从本质上提高性能,但是让PK starttime开头会提高性能。让我们这样做:

CREATE TABLE IF NOT EXISTS `sessions` (
  id INT UNSIGNED NOT NULL AUTO_INCREMENT,   -- note
  `starttime` datetime NOT NULL,
  `ip` varchar(39) NOT NULL CHARACTER SET ascii default '',  -- note
  `country_name` varchar(45) default '',
  `country_iso_code` char(2) CHARACTER SET ascii  default '',  -- note
  `org` varchar(128) default '',
  PRIMARY KEY(starttime, id)  -- in this order
  INDEX(id)                   -- to keep AUTO_INCREMENT happy
  -- The rest are unnecessary for the queries in question:
  KEY (`ip`),
  KEY (`starttime`),
  KEY (`country_name`)
) ENGINE=InnoDB;        -- just in case you are accidentally getting MyISAM

为什么?这将利用PK与数据的“聚类”优势。这样,将只扫描表中在该时间范围内的一部分。并且索引和数据之间不会出现反弹。而且您不需要很多索引即可有效地处理所有情况。

IPv6最多需要39个字节。请注意,VARCHAR不允许您进行任何范围(CDR)测试。我可以进一步讨论您喜欢的事情。