满足 GROUP BY
子句的最通用方法是扫描整个表并创建一个新的临时表,其中每个组的所有行都是连续的,然后使用此临时表来发现组并应用聚合函数(如果有)。在某些情况下,MySQL 能够做得比这更好,并通过使用索引访问来避免创建临时表。
使用索引进行 GROUP BY
最重要的先决条件是所有 GROUP BY
列引用同一索引中的属性,并且索引按顺序存储其键(例如,对于 BTREE
索引,但不是对于 HASH
索引)。是否可以使用索引访问替换临时表的创建,还取决于查询中使用索引的哪些部分、为这些部分指定的条件以及所选的聚合函数。
有两种方法可以通过索引访问执行 GROUP BY
查询,如下节所述。第一种方法将分组操作与所有范围谓词(如果有)一起应用。第二种方法首先执行范围扫描,然后对结果元组进行分组。
松散索引扫描也可以在某些条件下没有 GROUP BY
的情况下使用。请参阅 跳过扫描范围访问方法.
当使用索引直接检索分组列时,处理 GROUP BY
的最有效方法是。使用这种访问方法,MySQL 使用某些索引类型的属性,即键是有序的(例如,BTREE
)。此属性允许在索引中使用查找组,而不必考虑满足所有 WHERE
条件的索引中的所有键。这种访问方法只考虑索引中一小部分键,因此称为 松散索引扫描。当没有 WHERE
子句时,松散索引扫描会读取与组数量一样多的键,这可能远远小于所有键的数量。如果 WHERE
子句包含范围谓词(请参阅 range
连接类型在 第 10.8.1 节,“使用 EXPLAIN 优化查询” 中的讨论),松散索引扫描会查找满足范围条件的每个组的第一个键,并再次读取最少的键。这在以下情况下是可能的
查询在单个表上。
The
GROUP BY
仅命名构成索引最左侧前缀的列,而没有其他列。(如果查询包含DISTINCT
子句而不是GROUP BY
,所有不同的属性都引用构成索引最左侧前缀的列。)例如,如果表t1
在(c1,c2,c3)
上有索引,则如果查询包含GROUP BY c1, c2
,则松散索引扫描适用。如果查询包含GROUP BY c2, c3
(这些列不是最左侧前缀)或GROUP BY c1, c2, c4
(c4
不在索引中),则它不适用。在 select 列表(如果有)中使用的唯一聚合函数是
MIN()
和MAX()
,并且它们都引用同一列。该列必须在索引中,并且必须紧跟在GROUP BY
中的列之后。索引中除
GROUP BY
中引用的那些部分之外的所有部分必须是常量(即,它们必须在与常量的等式中引用),除了MIN()
或MAX()
函数的参数。对于索引中的列,必须索引完整的列值,而不仅仅是前缀。例如,对于
c1 VARCHAR(20), INDEX (c1(10))
,索引只使用c1
值的前缀,不能用于松散索引扫描。
如果松散索引扫描适用于查询,则 EXPLAIN
输出在 Extra
列中显示 Using index for group-by
。
假设在表 t1(c1,c2,c3,c4)
上有一个索引 idx(c1,c2,c3)
。松散索引扫描访问方法可用于以下查询
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;
以下查询无法使用这种快速选择方法执行,原因如下
松散索引扫描访问方法可以应用于选择列表中其他形式的聚合函数引用,除了已经支持的 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)
。松散索引扫描访问方法可用于以下查询
SELECT COUNT(DISTINCT c1), SUM(DISTINCT c1) FROM t1;
SELECT COUNT(DISTINCT c1, c2), COUNT(DISTINCT c2, c1) FROM t1;
紧密索引扫描可以是完整的索引扫描或范围索引扫描,具体取决于查询条件。
当不满足松散索引扫描的条件时,仍然有可能避免为 GROUP BY
查询创建临时表。如果 WHERE
子句中存在范围条件,则此方法只读取满足这些条件的键。否则,它执行索引扫描。由于此方法读取 WHERE
子句定义的每个范围内的所有键,或者在没有范围条件的情况下扫描整个索引,因此称为 紧密索引扫描。使用紧密索引扫描,分组操作仅在找到满足范围条件的所有键之后执行。
为了使此方法起作用,对于引用位于 GROUP BY
键之前或之间的键部分的查询中的所有列,存在一个常量相等条件就足够了。相等条件中的常量填充搜索键中的任何 “间隙”,以便可以形成索引的完整前缀。然后,这些索引前缀可用于索引查找。如果 GROUP BY
结果需要排序,并且可以形成索引的前缀的搜索键,MySQL 也将避免额外的排序操作,因为在有序索引中使用前缀搜索已经按顺序检索所有键。
假设在表 t1(c1,c2,c3,c4)
上有一个索引 idx(c1,c2,c3)
。以下查询不适用于之前描述的松散索引扫描访问方法,但仍适用于紧密索引扫描访问方法。
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;