满足 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 优化查询” 中的讨论),松散索引扫描会查找满足范围条件的每个组的第一个键,并且再次读取尽可能少的键。这在以下情况下是可能的
查询仅针对单个表。
GROUP BY
仅命名形成索引的最左侧前缀的列,而没有其他列。(如果查询具有DISTINCT
子句,而不是GROUP BY
,则所有不同的属性都引用形成索引的最左侧前缀的列。)例如,如果表t1
在(c1,c2,c3)
上有一个索引,则如果查询具有GROUP BY c1, c2
,则松散索引扫描适用。如果查询具有GROUP BY c2, c3
(列不是最左侧前缀)或GROUP BY c1, c2, c4
(c4
不在索引中),则它不适用。选择列表中使用的唯一聚合函数(如果有)是
MIN()
和MAX()
,并且它们都引用同一列。该列必须在索引中,并且必须紧跟GROUP BY
中的列。除了
MIN()
或MAX()
函数的参数外,索引中除GROUP BY
中引用的部分以外的所有部分都必须是常量(也就是说,它们必须在与常量的等式中引用)。对于索引中的列,必须索引完整列值,而不仅仅是前缀。例如,对于
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
结果需要排序,并且可以形成索引的前缀搜索键,则 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;