本文翻译自MySQL 8.0 Reference Manual 8.2.1.17 GROUP BY Optimization
执行group by语句最一般的方式就是扫描整张表,并且创建一张临时表来保存所有的分组字段值,然后通过这张临时表整理分组并执行聚集函数。在某些情况下MySQL可以通过使用索引访问避免创建临时表,依次提高查询性能。
通过索引来执行group by查询的一个最重要的前提条件是group by子句中所有的列都来自同一个索引,并且该索引按顺序保存列值(比如BTREE索引,HASH索引则不满足此约束)。是否使用索引访问而不是临时表也取决于使用索引中的哪些列,使用的条件类型以及使用的聚集函数等。
MySQL支持两种访问方式来通过索引执行group by语句,下面的小结将进行详细介绍。第一种方法在所有的范围条件上执行分组操作,第二种方法首先进行范围扫描,然后对获得的元组进行分组操作。
- Loose Index Scan(松散的索引扫描)
- Tight Index Scan(紧凑的索引扫描)
下面将详细介绍着两种索引扫描方法。
- Loose Index Scan(松散的索引扫描)
处理group by最高效的方式就是使用索引直接获取分组的列。当使用索引访问方法时,MySQL利用索引按照顺序组织列值的特性(比如BTREE索引)。这种特性可以被用来查找索引中满足条件的分组而不需要访问索引中的所有值。这种访问方法仅仅考虑索引中的部分值,所以称为松散的索引扫描(Loose Index Scan)。当查询语句中没有任何条件子句时,松散的索引扫描仅仅读取索引中分组数量个索引值,而不需要读取索引中的所有值,这大大地减少了读取的索引值数据。如果WHERE
子句中包含范围条件子句(jion type为range
),松散的索引扫描会读取满足条件的所有分组中的第一个key值,然后尽可能少的读取其他的key值。
要使用松散的所有扫描,必须满足如下条件:
查询仅仅在一张表上进行。
group by使用的列必须来自一个索引的最左前缀列,并且没有其他列(除了group by,对于有
DISTINCT
子句的查询,当所有的distinct列都来自一个索引的最左前缀列时,也满足条件)。例如,如果表t1
在(c1, c2, c3)
上定义了索引,可以对GROUP BY c1, c2
使用松散的索引扫描,但是对于GROUP BY c2, c3
却不可以使用松散的所有扫描,因为group by
子句中的列不是索引的最左前缀列,语句GROUP BY c1, c2, c4
也不能使用松散的所有扫描,因为c4
不是索引中的列。查询中只能使用
MIN()
或者MAX()
聚集函数,并且查询中使用的多个聚集函数只能指定相同的列。聚集函数中使用的列必须是使用的索引中的列,并且是GROUP BY
子句中出现的第一个列。(译者注:这条约束可以从BTREE索引的实现原理上理解)。索引中其他没有在
GROUP BY
子句中指定的列必须是常数(即必须和常数相等),除非是用于聚集函数MIN()
或者MAX()
的列。对于索引中的列,这些列必须是完整索引,而不是仅仅对列的部分值进行索引。例如:对于列
c1 VARCHAR(20), INDEX(c1(10))
,这个索引仅仅使用了c1
列的前面10个字符进行索引,因此不能使用松散的索引扫描进行优化。
如果可用使用松散的索引扫描处理查询语句,那么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 t1 < 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;
下面的语句则不能使用松散的索引扫描:
SELECT c1, SUM(c2) FROM t1 GROUP BY c1;
上述语句使用了SUM()
函数,而松散的索引扫描只支持MIN()
及MAX()
函数。
SELECT c1, c2 FROM t1 GROUP BY c2, c3;
上述语句GROUP BY
子句中的列不是索引的最左前缀列。
SELECT c1, c3 FROM t1 GROUP BY c1, c2;
上述语句中使用了同一索引中没有用于GROUP BY
子句中的列,但是该列没有和常数相等的条件。如果查询有WHERE c3 = const
的条件子句,则可以使用松散的索引扫描。
某些情况下,除了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;
- Tight Index Scan(紧凑的索引扫描)
紧凑的索引扫描可以是全索引扫描或者是范围索引扫描,这取决于查询的条件。
当松散索引扫描的条件没有满足时,仍然有避免创建临时表处理GROUP BY
查询的可能。如果WHERE
子句中有范围条件,这种访问方法仅仅读取那些满足条件的keys,否则会进行一次索引扫描。因为这种访问方法读取满足WHERE
子句条件的所有keys或者当没有WHERE
子句时则读取所有的keys,因此称其为紧凑的索引扫描。在使用紧凑的索引扫描时,仅当所有满足条件的keys都被读取之后才会进行分组操作(译者注:松散的索引扫描在扫描(或者读取)的过程中就进行了分组)。
为了使用紧凑的索引扫描,在GROUP BY
子句之前或之间的所有列都具有等于常数的条件表达式即可,即需要等于常数的条件来填充所有不满足最左前缀列的约束即可。这些被填充、没有间隙的索引可以被用来进行数据查找。如果GROUP BY
需要排序,则MySQL也可以通过此方法避免额外的排序操作。
假设在表t1(c1, c2, c3, c4)
有索引idx(c1, c2, c3)
下面的查询不能使用松散的索引扫描,但是依然可以使用紧凑的索引扫描:
SELECT c1, c2, c3 FROM t1 WHERE c2 = 'a' GROUP BY c1, c3;
上述语句中在使用索引时具有“间隙”,但是可以被条件c2 = 'a'
填充这个间隙。
SELECT c1, c2, c3 FROM t1 WHERE c1 = 'a' GROUP BY c2, c3;
上述语句没有满足最左前缀索引列的条件,但是条件c1 = 'a'
使得使用的索引的最左列被覆盖。