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

Very simple AVG() aggregation query on MySQL server takes ridiculously long time

我正在使用 MySQL 通过亚马逊服务提供的服务器,使用默认设置。 mytable涉及的table属于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 的索引是:

+---------------+------------+-----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| 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";

return 计数只需要 0.15 秒。这个特定查询的计数是692792;其他 date 的计数相似。

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

如有任何建议,我们将不胜感激!

对于 MyISAM tables,如果 SELECT 从一个 table 中检索,则 COUNT(*) 很快被优化为 return,没有检索到其他列,并且没有 WHERE 子句。

例如:

SELECT COUNT(*) FROM student;

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

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

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

要计算未包含在索引中的列的平均值,MySQL 将再次使用索引查找该日期的所有行(与之前相同)。但此外,对于它找到的每一行,它都必须读取该行的实际 table 数据,这意味着使用主键来定位该行,读取一些字节,并重复这 700k 次。这个"random access" is a lot slower than the sequential read in the first case. (This gets worse by the problem that "some bytes" is the innodb_page_size(默认16KB),所以你可能需要读取最多700k * 16KB = 11GB,相比"some MB" for count(*);并且根据您的内存配置,其中一些数据可能不会被缓存,必须从磁盘读取。)

一个解决方案是在索引中包含所有使用的列 (a "covering index"),例如在 date, 01 上创建索引。然后 MySQL 不需要访问 table 本身,并且可以继续,类似于第一种方法,只需读取索引即可。索引的大小会增加一点,所以 MySQL 将需要读取 "some more MB"(并执行 avg 操作),但它应该仍然是几秒钟的事情。

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

为了避免 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、...),但总的来说,它们将需要更多 space,但可能会快一点(因为它们各自较小)。但缓冲池可能仍然倾向于全索引,具体取决于使用模式和内存配置等因素,因此您可能需要对其进行测试。

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

正如您所提到的,另一种选择是构建一个摘要 table 在其中存储预先计算的值,特别是如果您不添加或修改过去日期的行(或者可以保留它们 up-to-date带扳机)。