MySQL 服务器上非常简单的 AVG() 聚合查询需要很长时间

Posted

技术标签:

【中文标题】MySQL 服务器上非常简单的 AVG() 聚合查询需要很长时间【英文标题】:Very simple AVG() aggregation query on MySQL server takes ridiculously long time 【发布时间】:2018-08-30 01:00:06 【问题描述】:

我正在使用默认设置通过 Amazon 服务的 mysql 服务器。涉及mytable 的表是InnoDB 类型,大约有10 亿行。 查询是:

select count(*), avg(`01`) from mytable where `date` = "2017-11-01";

执行大约需要 10 分钟。我在date 上有一个索引。此查询的EXPLAIN 是:

+----+-------------+---------------+------+---------------+------+---------+-------+---------+-------+
| id | select_type | table         | type | possible_keys | key  | key_len | ref   | rows    | Extra |
+----+-------------+---------------+------+---------------+------+---------+-------+---------+-------+
|  1 | SIMPLE      | mytable       | ref  | date          | date | 3       | const | 1411576 | NULL  |
+----+-------------+---------------+------+---------------+------+---------+-------+---------+-------+

这个表的索引是:

+---------------+------------+-----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table         | Non_unique | Key_name  | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+---------------+------------+-----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| mytable       |          0 | PRIMARY   |            1 | ESI         | A         |    60398679 |     NULL | NULL   |      | BTREE      |         |               |
| mytable       |          0 | PRIMARY   |            2 | date        | A         |  1026777555 |     NULL | NULL   |      | BTREE      |         |               |
| mytable       |          1 | lse_cd    |            1 | lse_cd      | A         |     1919210 |     NULL | NULL   | YES  | BTREE      |         |               |
| mytable       |          1 | zone      |            1 | zone        | A         |      732366 |     NULL | NULL   | YES  | BTREE      |         |               |
| mytable       |          1 | date      |            1 | date        | A         |    85564796 |     NULL | NULL   |      | BTREE      |         |               |
| mytable       |          1 | ESI_index |            1 | ESI         | A         |     6937686 |     NULL | NULL   |      | BTREE      |         |               |
+---------------+------------+-----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+

如果我删除AVG()

select count(*) from mytable where `date` = "2017-11-01";

返回计数仅需 0.15 秒。此特定查询的计数为 692792;其他dates 的计数相似。

我没有超过 01 的索引。这是一个问题吗?为什么AVG() 需要这么长时间来计算?一定是我做的不对。

欢迎提出任何建议!

【问题讨论】:

对于性能问题,请始终添加您的索引(以及解释计划,尽管此处不需要)。如果您使用 MyISAM 或 InnoDB,这也可能是相关的。您是否尝试重复您的查询?这里可能涉及缓存(因此时间不能直接比较)。另外:每个date 有多少行?我假设大约100k或更多?我还假设您在date 上有一个索引(这可以加快您的count(*)),但这不是您的主键的第一部分吗?尝试使用覆盖索引mytable(date, 01),它应该会加快您的查询速度(在这些假设下)。 谢谢,我相应地编辑了问题。我想我在date 上有两个索引,如编辑后的问题所示。我确实尝试重复我的查询。 关于“覆盖索引”:我有从0124 的列,我需要为所有这些列计算AVG()。将它们全部编入索引是否仍然可行? 【参考方案1】:

要计算具有特定日期的行数,MySQL 必须在索引中找到该值(这非常快,毕竟这是索引的目的),然后读取后续条目 index 直到找到下一个日期。根据esi 的数据类型,这将总结为读取一些 MB 数据来计算您的 700k 行。读取一些 MB 不会花费太多时间(而且这些数据甚至可能已经缓存在缓冲池中,具体取决于您使用索引的频率)。

为了计算未包含在索引中的列的平均值,MySQL 将再次使用索引查找该日期的所有行(与以前相同)。但此外,对于它找到的每一行,它都必须读取该行的实际表数据,这意味着使用主键来定位该行,读取一些字节,并重复这 700k 次。这个"random access" 比第一种情况下的顺序读取慢很多。 (由于“一些字节”是innodb_page_size(默认情况下为 16KB)的问题,情况变得更糟,因此您可能必须读取多达 700k * 16KB = 11GB,而 count(*) 的“一些 MB”;并且取决于根据您的内存配置,其中一些数据可能没有被缓存,必须从磁盘读取。)

解决方案是在索引中包含所有使用的列(“覆盖索引”),例如在date, 01 上创建索引。然后 MySQL 不需要访问表本身,并且可以继续,类似于第一种方法,只需读取索引即可。索引的大小会增加一点,因此 MySQL 需要读取“更多 MB”(并执行avg-操作),但这仍然应该是几秒钟的问题。

在 cmets 中,您提到需要计算 24 列的平均值。如果您想同时计算多个列的avg,则需要所有列的覆盖索引,例如date, 01, 02, ..., 24 防止表访问。请注意,包含所有列的索引需要与表本身一样多的存储空间(并且创建这样的索引需要很长时间),因此它可能取决于该查询是否值得这些资源的重要性。

为避免MySQL-limit of 16 columns per index,您可以将其拆分为两个索引(和两个查询)。创建例如索引date, 01, .., 12date, 13, .., 24,然后使用

select * from (select `date`, avg(`01`), ..., avg(`12`) 
               from mytable where `date` = ...) as part1
cross join    (select avg(`13`), ..., avg(`24`) 
               from mytable where `date` = ...) as part2;

请务必妥善记录这一点,因为没有明显的理由以这种方式编写查询,但这样做可能是值得的。

如果您只对单个列进行平均,您可以添加 24 个单独的索引(在 date, 01date, 02、...上),虽然它们总共需要更多空间,但可能会有点快一点(因为它们单独较小)。但缓冲池可能仍然偏爱完整索引,具体取决于使用模式和内存配置等因素,因此您可能需要对其进行测试。

由于date 是您的主键的一部分,您还可以考虑将主键更改为date, esi。如果您通过主键查找日期,则不需要额外的步骤来访问表数据(因为您已经访问了表),因此行为类似于覆盖索引。但这是对您的表的重大更改,可能会影响所有其他查询(例如,使用 esi 定位行),因此必须仔细考虑。

正如您所提到的,另一种选择是构建一个汇总表,您可以在其中存储预先计算的值,特别是如果您不添加或修改过去日期的行(或者可以通过触发器使它们保持最新)。

【讨论】:

谢谢!我学到了很多,现在我明白了为什么它很慢。我无法将主键的顺序更改为date, esi,因为在问题中我简化了查询。我确实需要esi 首先针对一组esis。我将尝试添加覆盖索引并在此处报告性能。 事实证明我无法为所有列添加覆盖索引。我收到一个错误“指定的关键部件太多;最多允许 16 个部件”。我想我必须寻找替代方法。由于我只关心平均值,因此我正在考虑创建另一个仅包含平均值的表。这将需要一两天,但在那之后,小表上的所有查询都会很快。 是的,你是对的,我忘记了这个限制;我为此添加了一个解决方案(拆分索引),但汇总表也可能是一个合适的解决方案(并且可以节省大量资源),如果您可以使其保持最新状态。【参考方案2】:

对于 MyISAM 表,如果 SELECT 从一个表中检索,没有检索到其他列并且没有 WHERE 子句,则 COUNT(*) 被优化为非常快速地返回。

例如:

从学生中选择 COUNT(*);

https://dev.mysql.com/doc/refman/5.6/en/group-by-functions.html#function_count

如果您添加 AVG() 或其他内容,您将失去此优化

【讨论】:

"并且没有 WHERE 子句。"不适用于此处。 感谢您的回答!我的表使用 InnoDB 引擎。你所说的对于 InnoDB 仍然适用吗? @ChunjiWang 为了处理 SELECT COUNT(*) 语句,InnoDB 扫描表的索引,如果索引不完全在缓冲池中,则需要一些时间。为了更快地计数,您可以创建一个计数器表并让您的应用程序根据它所做的插入和删除来更新它。但是,在数千个并发事务正在启动对同一个计数器表的更新的情况下,此方法可能无法很好地扩展。如果近似行数足够,则可以使用 SHOW TABLE STATUS。 dev.mysql.com/doc/refman/5.6/en/innodb-restrictions.html

以上是关于MySQL 服务器上非常简单的 AVG() 聚合查询需要很长时间的主要内容,如果未能解决你的问题,请参考以下文章

26《MySQL 教程》聚合函数(聚合函数 MIN、MAX)

mysql之聚合函数

mysql聚合

Day458.聚合函数 -mysql

在 MongoDB 中组合组 - 聚合

如何将mysql行以简单的方式转换为列