10.2.1.17 GROUP BY 优化
满足GROUP BY
子句的最一般方法是扫描整个表,创建一个临时表,其中每个组中的所有行都是连续的,然后使用这个临时表来发现组和应用聚合函数(如果有)。在某些情况下,MySQL 可以做得更好,不需要创建临时表,而是使用索引访问。
使用索引访问GROUP BY
的最重要前提是所有GROUP BY
列都引用同一个索引的属性,并且该索引存储其键顺序(例如,BTREE 索引,但不是 HASH 索引)。是否可以将临时表替换为索引访问还取决于查询中使用的索引部分、这些部分的条件和选择的聚合函数。
有两种方式可以通过索引访问执行GROUP BY
查询,详见以下各节。第一个方法同时应用分组操作和所有范围谓词(如果有)。第二个方法首先执行范围扫描,然后对结果进行分组。
Loose Index Scan 也可以在 absence of GROUP BY
下使用某些条件下。见Skip Scan Range Access Method
处理GROUP BY
的最有效方法是当索引直接检索分组列时。使用这种访问方法,MySQL 使用某些索引类型的特性,即键值有序(例如,BTREE
)。这种特性使得可以在索引中查找组群,而不需要考虑所有满足WHERE
条件的键。这种访问方法只考虑索引中的少数键,因此称为Loose Index Scan。当没有WHERE
子句时,Loose Index Scan 读取的键数量与组群数量相等,这可能是一个比所有键小得多的数字。如果WHERE
子句包含范围谓词(请参阅range
join type 在第10.8.1节,“使用 EXPLAIN 优化查询”中讨论),Loose Index Scan 将查找每个组群的第一键,使其满足范围条件,然后再读取最小可能数量的键。这是以下情况下可能的:
-
查询只涉及单个表。
-
只有在索引的左最前缀中形成的列名才能被
GROUP BY
语句使用。如果查询中有DISTINCT
子句,而不是GROUP BY
,那么所有distinct属性都指向索引的左最前缀中的列。例如,如果表t1
在(c1,c2,c3)
上有索引,那么 Loose Index Scan 可以应用于查询GROUP BY c1, c2
,但不能应用于查询GROUP BY c2, c3
(列不是左最前缀)或GROUP BY c1, c2, c4
(c4
不在索引中)。 -
如果查询的选择列表中使用了聚合函数,那么这些函数只能是
MIN()
和MAX()
,并且所有这些函数都引用同一个列。该列必须在索引中,并且必须紧随GROUP BY
中的列。 -
除了
GROUP BY
语句中指定的列外,索引中的其他部分都必须是常量(即它们必须在等式中与常量进行引用),除非它们是MIN()
或MAX()
函数的参数。 -
对于索引中的列,必须将完整的列值索引,而不是只索引前缀。例如,以
c1 VARCHAR(20), INDEX (c1(10))
为例,索引只使用了c1
值的前缀,因此不能用于Loose Index Scan。
如果 Loose Index Scan 适用于查询,EXPLAIN 输出将在 Extra
列中显示Using index for group-by
。
假设表 t1(c1,c2,c3,c4) 上存在索引 idx(c1,c2,c3)。 Loose Index Scan 访问方法可以用于以下查询:
SELECT c1, c2 FROM t1 GROUP BY c1, c2;
SELECT DISTINCT c1, c2 FROM t1;
SELECT c1, MIN(c2) FROM t1 GROUP BY c1;
SELECT c1, c2 FROM t1 WHERE c1 < const GROUP BY c1, c2;
SELECT MAX(c3), MIN(c3), c1, c2 FROM t1 WHERE c2 > const GROUP BY c1, c2;
SELECT c2 FROM t1 WHERE c1 < const GROUP BY c1, c2;
SELECT c1, c2 FROM t1 WHERE c3 = const GROUP BY c1, c2;
以下查询不能使用快速选择方法,因为原因如下:
Loose Index Scan 访问方法可以应用于 select 列表中的其他聚合函数引用,除了已经支持的 MIN()
和 MAX()
引用之外。
-
AVG(DISTINCT)
,SUM(DISTINCT)
和COUNT(DISTINCT)
是支持的。AVG(DISTINCT)
和SUM(DISTINCT)
只能接受一个参数。COUNT(DISTINCT)
可以有多个列参数。 -
查询中不能包含
GROUP BY
或DISTINCT
子句。 -
之前描述的松散索引扫描限制仍然适用。
假设表t1(c1,c2,c3,c4)
上存在索引idx(c1,c2,c3)
。 Loose Index Scan访问方法可以用于以下查询:
SELECT COUNT(DISTINCT c1), SUM(DISTINCT c1) FROM t1;
SELECT COUNT(DISTINCT c1, c2), COUNT(DISTINCT c2, c1) FROM t1;
紧凑索引扫描可能是全索引扫描或范围索引扫描,取决于查询条件。
当 Loose Index Scan 的条件不满足时,仍然可能避免创建临时表以执行 GROUP BY
查询。如果 WHERE 子句中存在范围条件,这种方法只读取满足这些条件的键。否则,它将执行索引扫描。由于这种方法在每个范围定义中读取所有键,或者如果没有范围条件,则扫描整个索引,因此称为 Tight Index Scan。使用 Tight Index Scan 时,分组操作仅在找到满足范围条件的所有键后执行。
为了使这种方法工作,只需确保查询中对 key 的部分具有常量等式条件,这些等式条件来自于 GROUP BY 关键字前或之间的列。这些等式条件填充索引搜索键中的““gaps””,从而使得可以形成完整的索引前缀,然后可以用于索引查找。如果 GROUP BY 结果需要排序,并且可以形成搜索键,这些搜索键是索引的前缀,MySQL 也避免了额外的排序操作,因为在有序索引中搜索前缀已经返回所有键的顺序。
假设表 t1( c1, c2, c3, c4 )
上存在索引 idx(c1,c2,c3)
。以下查询不能使用前述 Loose Index Scan 访问方法,但仍然可以使用 Tight Index Scan 访问方法。
-
在
GROUP BY
中存在空隙,但是由条件c2 = 'a'
覆盖:SELECT c1, c2, c3 FROM t1 WHERE c2 = 'a' GROUP BY c1, c3;
-
GROUP BY 子句不从键的第一部分开始,但是有一种情况提供了该部分的常量:
SELECT c1, c2, c3 FROM t1 WHERE c1 = 'a' GROUP BY c2, c3;