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

chu*_*jiw 3 mysql aggregation

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

select count(*), avg(`01`) from mytable where `date` = "2017-11-01";
Run Code Online (Sandbox Code Playgroud)

这需要将近 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  |
+----+-------------+---------------+------+---------------+------+---------+-------+---------+-------+
Run Code Online (Sandbox Code Playgroud)

该表中的索引是:

+---------------+------------+-----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| 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      |         |               |
+---------------+------------+-----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
Run Code Online (Sandbox Code Playgroud)

如果我删除AVG()

select count(*) from mytable where `date` = "2017-11-01";
Run Code Online (Sandbox Code Playgroud)

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

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

任何建议表示赞赏!

Sol*_*are 6

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

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

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

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

为了避免每个索引 16 列MySQL 限制,您可以将其拆分为两个索引(和两个查询)。创建例如索引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;
Run Code Online (Sandbox Code Playgroud)

确保很好地记录这一点,因为没有明显的理由以这种方式编写查询,但这可能是值得的。

如果您只对单列求平均值,则可以添加 24 个单独的索引(在date, 01, date, 02, ... . 但是缓冲池可能仍然支持完整索引,具体取决于使用模式和内存配置等因素,因此您可能需要对其进行测试。

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

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