MySQL优化GROUP BY-松散索引扫描与紧凑索引扫描

一、松散索引扫描 和 紧凑索引扫的描述

1、松散索引扫描:使用索引时最有效的途径是直接搜索组域。通过该访问方法,MySQL使用某些关键字排序的索引类型(例如,B-树)的属性。该属性允许使用 索引中的查找组而不需要考虑满足所有WHERE条件的索引中的所有关键字。既然该访问方法只考虑索引中的关键字的一小部分,它被称为松散索引扫描。如果没有WHERE子句, 松散索引扫描读取的关键字数量与组数量一样多,可以比所有关键字数小得多。如果WHERE子句包含范围判断式, 松散索引扫描查找满足范围条件的每个组的第1个关键字,并且再次读取尽可能最少数量的关键字 。

2、紧凑索引扫描:紧凑索引扫描可以为索引扫描或一个范围索引扫描,取决于查询条件。

如果不满足松散索引扫描条件,GROUP BY查询仍然可以不用创建临时表。如果WHERE子句中有范围条件,该方法只读取满足这些条件的关键字。否则,进行索引扫描。该方法读取由WHERE子句定义的每个范围的所有关键字,或没有范围条件式扫描整个索引,我们将它定义为紧凑式索引扫描。请注意对于紧凑式索引扫描,只有找到了满足范围条件的所有关键字后才进行组合操作。要想让该方法工作,对于引用GROUP BY关键字元素的前面、中间关键字元素的查询中的所有列,有一个常量等式条件即足够了。等式条件中的常量填充了搜索关键字中的“差距”,可以形成完整的索引前缀。这些索引前缀可以用于索引查找。如果需要排序GROUP BY结果,并且能够形成索引前缀的搜索关键字,MySQL还可以避免额外的排序操作,因为使用有顺序的索引的前缀进行搜索已经按顺序检索到了所有关键字。

紧凑索引扫描实现 GROUP BY 和松散索引扫描的区别主要在于他需要在扫描索引的时候,读取所有满足条件的索引键,然后再根据读取的数据来完成 GROUP BY 操作得到相应结果。

二、 实现

满足GROUP BY子句的最一般的方法是扫描整个表并创建一个新的临时表,表中每个组的所有行应为连续的,然后使用该临时表来找到组并应用累积函数(如果有)。在某些情况中,MySQL能够做得更好,即通过索引访问而不用创建临时表。

       为GROUP BY使用索引的最重要的前提条件是所有GROUP BY列引用同一索引的属性,并且索引按顺序保存其关键字。是否用索引访问来代替临时表的使用还取决于在查询中使用了哪部分索引、为该部分指定的条件,以及选择的累积函数。

       由于GROUP BY 实际上也同样会进行排序操作,而且与ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作。当然,如果在分组的时候还使用了其他的一些聚合函数,那么还需要一些聚合函数的计算。所以,在GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引。在MySQL 中,GROUP BY 的实现同样有多种三种方式,其中有两种方式会利用现有的索引信息来完成 GROUP BY,另外一种为完全无法使用索引的场景下使用。下面我们分别针对这三种实现方式做一个分析。

1、使用松散索引扫描(Loose index scan)实现 GROUP BY

对“松散索引扫描”的定义

定义1:松散索引扫描,实际上就是当 MySQL 完全利用索引扫描来实现 GROUP BY 的时候,并不需要扫描所有满足条件的索引键即可完成操作得出结果。

定义2:优化Group By最有效的办法是当可以直接使用索引来完全获取需要group的字段。使用这个访问方法时,MySQL使用对关键字排序的索引的类型(比如BTREE索引)。这使得索引中用于group的字段不必完全涵盖WHERE条件中索引对应的key。由于只包含索引中关键字的一部分,因此称为松散的索引扫描。

意思是索引中用于group的字段,没必要包含多列索引的全部字段。例如:有一个索引idx(c1,c2,c3),那么group by c1、group by c1,c2这样c1或c1、c2都只是索引idx的一部分。要注意的是,索引中用于group的字段必须符合索引的“最左前缀”原则。group by c1,c3是不会使用松散的索引扫描的。

要利用到松散索引扫描实现GROUP BY,需要至少满足以下几个条件:

查询针对一个单表。

GROUP BY 条件字段必须在同一个索引中最前面的连续位置

在使用GROUP BY 的同时,如果有聚合函数,只能使用 MAX 和 MIN 这两个聚合函数,并且它们均指向相同的列。

如果引用(where条件中)到了该索引中GROUP BY 条件之外的字段条件的时候,必须以常量形式存在,但MIN()或MAX() 函数的参数例外;
   或者说:索引的任何其它部分(除了那些来自查询中引用的GROUP BY)必须为常数(也就是说,必须按常量数量来引用它们),但MIN()或MAX() 函数的参数例外。

补充:如果sql中有where语句,且select中引用了该索引中GROUP BY 条件之外的字段条件的时候,where中这些字段要以常量形式存在。

如果查询中有where条件,则条件必须为索引,不能包含非索引的字段


下面的查询提供该类的几个例子,假定表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;

例如:

CREATE TABLE `d_tf_ydfh_realtime_1` (
  `time_id` VARCHAR(8) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `qua_id` VARCHAR(5) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `strative_id` VARCHAR(8) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `org_no` VARCHAR(16) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `tdfdfh` DECIMAL(15,3) DEFAULT NULL,
  KEY `time_id` (`time_id`,`strative_id`,`tdfdfh`)
) ENGINE=MYISAM DEFAULT CHARSET=utf8


松散索引
EXPLAIN 
SELECT DISTINCT time_id,strative_id FROM d_tf_ydfh_realtime_1;
EXPLAIN 
SELECT time_id FROM d_tf_ydfh_realtime_1  GROUP BY time_id;
EXPLAIN 
SELECT time_id,strative_id FROM d_tf_ydfh_realtime_1  GROUP BY time_id,strative_id;
EXPLAIN 
SELECT time_id,MAX(tdfdfh),MIN(tdfdfh) FROM d_tf_ydfh_realtime_1  WHERE strative_id='3213'  GROUP BY time_id;
EXPLAIN 
SELECT time_id,strative_id,MAX(tdfdfh),MIN(tdfdfh) FROM d_tf_ydfh_realtime_1  WHERE strative_id<'3213' AND strative_id>'32'  GROUP BY time_id,strative_id;
EXPLAIN 
SELECT time_id,strative_id,MAX(tdfdfh),MIN(tdfdfh) FROM d_tf_ydfh_realtime_1  WHERE strative_id IN ('3213','32','3210')  GROUP BY time_id,strative_id;
EXPLAIN 
SELECT time_id,strative_id,MAX(tdfdfh),MIN(tdfdfh) FROM d_tf_ydfh_realtime_1 WHERE time_id='20110903'  GROUP BY time_id,strative_id;
EXPLAIN 
SELECT time_id,strative_id,MAX(tdfdfh),MIN(tdfdfh) FROM d_tf_ydfh_realtime_1 WHERE time_id<'20130101'  GROUP BY time_id,strative_id;
EXPLAIN 
SELECT time_id,strative_id,MAX(tdfdfh),MIN(tdfdfh) FROM d_tf_ydfh_realtime_1 WHERE time_id<'20130101' AND time_id>='20120101' GROUP BY time_id,strative_id;
EXPLAIN 
SELECT time_id,strative_id,MAX(tdfdfh),MIN(tdfdfh) FROM d_tf_ydfh_realtime_1 WHERE time_id='20110903' AND (strative_id>='32' AND strative_id<'3213') GROUP BY time_id,strative_id;

紧凑索引
EXPLAIN 
SELECT strative_id,MAX(tdfdfh),MIN(tdfdfh) FROM d_tf_ydfh_realtime_1 WHERE time_id='20110903' GROUP BY strative_id;
EXPLAIN 
SELECT time_id,tdfdfh FROM d_tf_ydfh_realtime_1 WHERE strative_id='32' GROUP BY time_id,tdfdfh;
EXPLAIN 
SELECT time_id,strative_id,tdfdfh FROM d_tf_ydfh_realtime_1 WHERE time_id='20110903' GROUP BY strative_id,tdfdfh;


不能用该快速选择方法执行下面的查询:

1、除了MIN()或MAX(),还有其它累积函数,例如: SELECT c1, SUM(c2) FROM t1 GROUP BY c1;

2、GROUP BY子句中的域不引用索引开头,如下所示: SELECT c1,c2 FROM t1 GROUP BY c2, c3;

3、查询引用了GROUP BY部分后面的关键字的一部分,并且没有等于常量的等式,例如:SELECT c1,c3 FROM t1 GROUP BY c1, c2;

这个例子中,引用到了c3(c3必须为组合索引中的一个),因为group by 中没有c3。并且没有等于常量的等式。所以不能使用松散索引扫描


为什么松散索引扫描的效率会很高?

答:因为在没有WHERE 子句,也就是必须经过全索引扫描的时候, 松散索引扫描需要读取的键值数量与分组的组数量一样多,也就是说比实际存在的键值数目要少很多。而在WHERE 子句包含范围判断式或者等值表达式的时候, 松散索引扫描查找满足范围条件的每个组的第1 个关键字,并且再次读取尽可能最少数量的关键字。

2、使用紧凑索引扫描(Tight index scan)实现 GROUP BY

紧凑索引扫描实现 GROUP BY 和松散索引扫描的区别主要在于:

紧凑索引扫描需要在扫描索引的时候,读取所有满足条件的索引键,然后再根据读取出的数据来完成 GROUP BY 操作得到相应结果。

这时候的执行计划的 Extra 信息中已经没有“Using index for group-by”了,但并不是说 MySQL 的 GROUP BY 操作并不是通过索引完成的,只不过是需要访问 WHERE 条件所限定的所有索引键信息之后才能得出结果。这就是通过紧凑索引扫描来实现 GROUP BY 的执行计划输出信息。

在 MySQL 中,MySQL Query Optimizer 首先会选择尝试通过松散索引扫描来实现 GROUP BY 操作,当发现某些情况无法满足松散索引扫描实现 GROUP BY 的要求之后,才会尝试通过紧凑索引扫描来实现。

当 GROUP BY 条件字段并不连续或者不是索引前缀部分的时候,MySQL Query Optimizer 无法使用松散索引扫描。这时检查where 中的条件字段是否有索引的前缀部分,如果有此前缀部分,且该部分是一个常量,且与group by 后的字段组合起来成为一个连续的索引。这时按紧凑索引扫描。

SELECT max(c3) FROM t1 WHERE c1=1 GROUP BY  c2;

需读取c1=1的所有数据,然后在读取的数据中完成group by操作得到结果。(这里group by 字段并不是一个连续索引,where 中c1正好弥补缺失的索引键,又恰好是一个常量,因此使用紧凑索引扫描)c1,c2这个顺序是可以使用该索引。如果连接的顺序不符合索引的“最左前缀”原则,则不使用紧凑索引扫描。


以下例子使用紧凑索引扫描

GROUP BY中有一个差距,但已经由条件c2= 1覆盖。

explain SELECT c1,c3 FROM t1 WHERE c2 = 1 GROUP BY c1,c3

GROUP BY不以关键字的第1个元素开始,但是有一个条件提供该元素的常量。
explain SELECT c1,c3 FROM t1 WHERE c1=1 GROUP BY  c2,c3;


下面的例子都不使用紧凑索引扫描

(1)、c2,c3连接起来并不符合索引“最左前缀”原则

explain SELECT c2,c3 FROM t1 WHERE c2=1 GROUP BY  c3;

(2)、c1,c3连接起来并不符合索引“最左前缀”原则

explain SELECT c1,c3 FROM t1 WHERE c1=1 GROUP BY  c3;

3、使用临时表实现 GROUP BY

MySQL Query Optimizer 发现仅仅通过索引扫描并不能直接得到 GROUP BY 的结果之后,他就不得不选择通过使用临时表然后再排序的方式来实现 GROUP BY了。在这样示例中即是这样的情况。 group_id 并不是一个常量条件,而是一个范围,而且 GROUP BY 字段为 user_id。所以 MySQL 无法根据索引的顺序来帮助 GROUP BY 的实现,只能先通过索引范围扫描得到需要的数据,然后将数据存入临时表,然后再进行排序和分组操作来完成 GROUP BY。

explain SELECT c1 FROM t1 WHERE c1 between 1 and 10 GROUP BY  c2;


-- 参考文献: http://blog.csdn.net/zm2714/article/details/7887093

                         http://www.cnmiss.cn/?p=373

                        http://tech.it168.com/a2009/0324/269/000000269430.shtml


你可能感兴趣的:(mysql,by,优化group,max和min,松散索引紧凑索引)