三、各查询实现原理

  查询是在最开始进行的。MySQl在设计时,采用了这样的思路:针对主要应用场景选择一个或几个性能优异的核心算法作为引擎,然后努力将一些非主要应用场景作为该算法的特例或变种植入到引擎当中。

 

1、join实现原理

2、select实现原理

3、order by 实现原理

4、group by 实现原理

5、distinct 实现原理

 

 

1、join实现原理

Join是select的核心,Join是根。

Join buffer:Block Nested-Loop Join (BNL)-----------join buffer
只有当join查询中有表用到以下查询:
All:全表扫描,
Index:全索引扫描,
Range:范围扫描,如 in(),id>18000,
index_merge:
的时候,join查询才会用到join buffer。
在MySQL 中只有一种 Join 算法: Nested Loop Join(没有其他很多数据库所提供的 Hash Join,也没有 Sort Merge Join)。

【驱动表】

用这个表作为第一个查询的表,其他表在这个基础上递归。

【Nested Loop Join】

    Nested Loop Join 就是通过驱动表的结果集作为循环基础数据,然后一条一条的通过该结果集中的数据作为过滤条件到下一个表中查询数据,然后合并结果。
如果还有第三个参与 Join,则再通过前两个表的 Join 结果集作为循环基础数据,再一次通过循环查询条件到第三个表中查询数据,如此往复。接下来通过一个三表
join查询来说明mysql的Nested Loop Join的实现方式。 select m.subject msg_subject, c.content msg_content
from user_group g,group_message m,group_message_content c where g.user_id = 1 and m.group_id = g.group_id and c.group_msg_id = m.id 使用explain看看执行计划
(Explain
/explain extended查看执行计划): explain select m.subject msg_subject, c.content msg_content from user_group g,group_message m, group_message_content c where g.user_id = 1 and m.group_id = g.group_id and c.group_msg_id = m.id\G; 结果如下: *************************** 1. row *************************** id: 1 select_type: SIMPLE table: g type: ref possible_keys: user_group_gid_ind,user_group_uid_ind,user_group_gid_uid_ind key: user_group_uid_ind key_len: 4 ref: const rows: 2 Extra: *************************** 2. row *************************** id: 1 select_type: SIMPLE table: m type: ref possible_keys: PRIMARY,idx_group_message_gid_uid key: idx_group_message_gid_uid key_len: 4 ref: g.group_id (根据g表的group_id查,所以要先查出g表记录,每条g表符合记录对m表递归) rows: 3 Extra: *************************** 3. row *************************** id: 1 select_type: SIMPLE table: c type: ref possible_keys: idx_group_message_content_msg_id key: idx_group_message_content_msg_id key_len: 4 ref: m.id (根据m表的id查,所以要先查出m表记录,每条m表符合条件记录递归c表) rows: 2 Extra: ****************************************************** 从结果可以看出,explain选择user_group作为驱动表,首先通过索引user_group_uid_ind来进行const条件的索引ref查找,然后用user_group表中过滤出来的结果集
group_id字段作为查询条件,对group_message循环查询,然后再用过滤出来的结果集中的group_message的id作为条件与group_message_content的group_msg_id
进行循环比较查询,获得最终的结果。 这个过程可以通过如下伪代码来表示:
for each record g_rec in table user_group that g_rec.user_id=1{ for each record m_rec in group_message that m_rec.group_id=g_rec.group_id{ for each record c_rec in group_message_content that c_rec.group_msg_id=m_rec.id pass the (g_rec.user_id, m_rec.subject, c_rec.content) row combination to output; } } 如果去掉group_message_content表上面的group_msg_id字段的索引,执行计划会有所不一样。 drop index idx_group_message_content_msg_id on group_message_content; explain select m.subject msg_subject, c.content msg_content from user_group g,group_message m, group_message_content c where g.user_id = 1 and m.group_id = g.group_id and c.group_msg_id = m.id\G; 得到的执行计划如下: *************************** 1. row *************************** id: 1 select_type: SIMPLE table: g type: ref possible_keys: user_group_uid_ind key: user_group_uid_ind key_len: 4 ref: const rows: 2 Extra: *************************** 2. row *************************** id: 1 select_type: SIMPLE table: m type: ref possible_keys: PRIMARY,idx_group_message_gid_uid key: idx_group_message_gid_uid key_len: 4 ref: g.group_id rows: 3 Extra: *************************** 3. row *************************** id: 1 select_type: SIMPLE table: c type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 96 Extra:Using where;Using join buffer ****************************************************** 因为删除了索引,所以group_message_content的访问从ref变成了ALL,keys相关的信息也变成了NULL,Extra信息也变成了Using Where和Using join buffer,
这时获取content内容只能通过对全表的数据进行where过滤才能获取。Using join buffer是指使用到了Cache,只有当join类型为ALL,index,range或者是index_merge
的时候才会使用join buffer,它的使用过程可以用下面代码来表示:
for each record g_rec in table user_group{ for each record m_rec in group_message that m_rec.group_id=g_rec.group_id{ put (g_rec, m_rec) into the join buffer if (buffer is full) flush_buffer(); } } flush_buffer(){ for each record c_rec in group_message_content that c_rec.group_msg_id = c_rec.id{ for each record in the join buffer pass (g_rec.user_id, m_rec.subject, c_rec.content) row combination to output; } empty the buffer; } 在实现过程中可以看到把user_group和group_message的结果集放到join buffer中,而不用每次user_group和group_message关联后马上和group_message_content
关联,这也是没有必要的;需要注意的是join buffer中只保留查询结果中出现的列值,它的大小不依赖于表的大小,我们在伪代码中看到当join buffer被填满后,mysql将会
flush buffer,flush buffer时将3者做关联并返回到输出output。

【针对join实现原理想到的->join语句的优化】

1、在query中用order by时,尽可能利用已有的索引避免实际的排序计算;
2、在有些 query 的优化过程中,为了避免实际的排序操作而调整索引字段的顺序,甚至是增加索引字段也是值得的。当然,在调整索前,同时还需要评估调整该索引对其
他 query 所带来的影响,平衡整体得失。
3、用小结果集驱动大结果集,尽量减少join语句中的Nested Loop的循环总次数;
4、优先优化Nested Loop的内层循环,因为内层循环是循环中执行次数最多的,每次循环提升很小的性能都能在整个循环中提升很大的性能;
5、对被驱动表的join字段上建立索引(前面看到,没有索引的要做全表扫描);
6、当被驱动表的join字段上无法建立索引的时候,设置足够的Join Buffer Size,当mysql对条件字段进行排序时,如果需要排序字段的总长度大于该参数的值的时候,
mysql就会对需要排序的字段使用临时表进行分段,这样也会有性能的消耗。

 

 

2、select实现原理

  MySQL的select查询中,核心功能就是JOIN查询。

  如select id, name from student;  select语句在MySQL中执行时会转换为JOIN来处理的。

  以sql:select A.id, B.score from student A left join subject B on A.id=B.id where A.age > 10 and B.score > 60;为例

    在上面的例子中,需要完成2个join:先join表A,再join表B(这里请注意,不是涉及几个表,就需要join几个表,MySQL的join优化还是挺强大的,具体解释见后)。在MySQL进行sql解析时,
会生成一个需要join的表的list,后面会挨个对该list的表进行join操作。
继续gdb,在sub_select函数中,可以看到这样一行代码:(*join_tab->read_first_record)(join_tab)这个就是读取表A的第一行结果,可以看join_tab里面的信息有表A的名字。
接下来就是很关键的一个函数:evaluate_join_record,这个函数主要做2件事:
    1)当前已经拿到的信息进行where条件计算,判断是否需要继续往下走;
    2)递归JOIN;

  还是以上面的sql为例,首先执行第一个join,此时会遍历表A的每一行结果,每遍历一个结果,会进行where条件的判断。

    这里需要注意:当前的where条件判断只会判断已经读出来的列,由于此时只读出来表A的数据,因此现在只能对第一个where条件,即A.age > 10进行判断,如果满足,
则递归调用join:sql_select.cc: 11037 rc=(*join_tab->next_select)(join, join_tab+1, 0);,这里的next_select函数就是sub_select,MySQL就是这
样来实现递归操作的。如果不满足,则不会递归join,而是继续到下一行数据,从而达到剪枝的目的。   继续跟下去,此时通过上面的next_select递归的又调用到sub_select上,同样会走上面的逻辑,即先read_first_record,然后evaluate_join_record,这里由于表A和表
B的数据都有了,于是可以对上面后面2个where条件:A.id
= B.id和B.score > 60进行判断了。到此,所有的where条件都已经判断完毕,如果当前行对3个where条件都满足,就可
以将结果输出。   以上就是select实现的大体过程,主要有2点,一个是join是采用递归实现的,另一个是每读一个表的数据,会将当前的where条件进行计算,剪枝。还有一个细节没有提到:MySQL
是如何进行where条件判断的?或者说,MySQL是如何进行表达式计算的?   答案就是前面提到的item类。当MySQL在解析时,会将sql解析为很多item,同时也会建立各个item之间的关系。对于表达式,会生成一棵语法树。比如表达式:B.score
> 60,
此时会生成3个item:B.score、>和60,其中B.score和60分别是>的左右孩子,这样,求表达式的值时,就是求>的val_int(),然后就会递归的调用左右子树的val_int(),再做比
较判断即可。   还有一个问题:如何求B.score的val_int()?对于此问题的答案我没有具体看过,根据之前一个同事的sql实现方式,我是这样推测的:B.score是数据表中的真实值,因此它的值
肯定是通过去表中获取。在item类中,有一个函数:fix_field,它是用于告诉外界,去哪里获取此item的值,往往在sql执行的预处理阶段调用。于是在预处理时,告诉该item去某个
固定buffer读取结果,同时,每当从表中读出一行数据时,将该数据保存在该buffer中,这样就可以将两者关联起来。这个部分纯属个人推测,感兴趣的同学可以自己根据源码看看。   再回到之前提到的一点,如果我们将sql稍微改一下:select A.id, B.score
from student A left join subject B on A.id=B.id where B.score > 60;,即去掉
第一个where条件,此时会发生什么?   答案是,MySQL会做一个优化,将sql转换为select B.id, B.score
from subject B where B.score > 60,这样就不需要A同B join的逻辑了。实际上最开始我在gdb时
就用的这条sql,结果死活看不到递归调用sub_select的场景,还以为原理不对,后来才发现是MySQL优化捣的乱。

  【type=const】

    被称为“常量”
驱动表或被驱动表,唯一索引的等值查询。
在整个查询过程中这个表最多只会有一条匹配的行,比如主键 id=1 就肯定只有一行,只需读取一次表数据便能取得所需的结果,且表数据在分解执行计划时读取。

  【type=index_merge】

    index merge-索引合并优化。
    MySQL 5.0 版本之前,每个表在查询时 只能使用一个索引,有些不知道此功能限制的开发总是在一个表上创建很多单独列的索引,以便当where条件中含有这些列是能够走上索引。
索引能够提供查询速度,但是也能给日常维护和IUD 操作带来维护成本。 MySQL
5.0 和之后的版本推出了一个新特性---索引合并优化(Index merge optimization),它让MySQL可以在查询中对一个表使用多个索引,对它们同时扫描,并且合并结
果。 http:
//blog.itpub.net/22664653/viewspace-774687   一般在多条件过滤时, 过滤的多个列上有单独的索引。   select * from xxx where a>xxx and c >xxxx;   当a, c列上都有索引,查询两个索引都走,然后要做index merge。   一般会调整为组合索引来避免merge.

  【type=index】

    索引全扫描。Index与all的区别就是index只遍历索引,不回表。
建表:
create table a(a_id int not null, key(a_id));
insert into a value(1),(2)
mysql> explain select * from a\G
...
         type: index

  【type=ref】

  驱动表或被驱动表,非唯一性索引访问。

  【type=eq_ref】

    被驱动表,使用唯一性索引查找(主键或唯一性索引)。

  【type=range】

    以范围的形式扫描索引,常见between,<,>,in查询。  

  【type=all】

    驱动表或被驱动表,全表扫描。

 

3、order by 实现原理

    真正的排序是在查询之后的。排序查找本质上还是查找(select),只是select完成后即可得到有序结果的属于第一种;select根据条件查找出的结果不符合order by条件的顺序时,还需要进一步排序的属于第二种,只有第二种才用到sort buffer。所以这部分要结合select实现原理考虑其执行计划。在 MySQL 中的ORDER BY有两种排序实现方式。

<第一种>
  是利用有序索引获取有序数据,这种直接从数据页取出来的数据就是有序的,这是所有order by中最优的排序方式了,不进行实际的排序,利用了索引组织结构的有序性,并不用
sort buffer进行专门排序。   这一类排序,query的order by条件和query的执行计划中所利用的index索引建完全一致,且索引访问方式为 rang、 ref 或者 index 的时候,MySQL 可以利用索引顺序
而直接取得已经有序的数据。
<第二种>   对于排序语句,如果不能直接从索引中获得结果,那么就要用到专门的排序算法,将取得的数据在sort buffer内存中进行排序。MySQL Query Optimizer 所给出的执行计划
(通过 EXPLAIN 命令查看)中被称为 filesort。

  【哪些情况会不用专门排序---即不用sort buffer】

1、查询条件字段和排序字段不一致。
(1)查询条件字段中没有索引,排序字段是主键。(InnoDB)
  Age字段上没有加索引,id为主键,会根据主键索引结构直接查找,其实还是做了全表扫描边查找边过滤出符合age条件的记录集。这里做全表扫描用到primary key,是因为数据页
本来就是由主键索引组织而成的,id本来就按照数据的先后顺序有序.主键的全索引扫描,此例为特殊的全表扫描。【如果查询条件上没有索引,非做全表扫描不可,除非有limit】。

2、查询条件字段和排序字段一致,且有索引。(InnoDB/MyISAM)

  【哪些情况会用sort buffer】

查询条件字段和排序字段一致:
(1)没有索引的字段排序:(InnoDB/MyISAM)
 Age字段上没有任何索引,先全表扫描符合条件的按数据页先后顺序都查出,然后在sort buffer中排序,返回。
【如果age字段上有二级索引,则会在二级索引上直接查找到有序主键,根据主键查找到数据列表,本身就是有序的,不需要sort buffer再排序】
 
查询条件字段和排序字段不一致排序:
(1)查询条件没有索引,排序字段是主键。(MyISAM)
MyISAM中主键索引和二级索引结构是一模一样的,数据页并不是在主键索引结构中组织的,所以不同于InnoDB,这类全表扫描完成后并不是按照主键有序的,查找结果完成后,
还需要在sort buffer中做排序即filesort。 (
2)查询条件是二级索引,排序字段是主键(InnoDB/MyISAM) Age字段有二级索引,按二级索引顺序查找,然后在sort buffer中按主键ID排序,返回。【不可能按照主键查找,因为按照二级索引查找更快,可能就一两条数据,按照id查找的话,
要整个聚簇索引都过一遍。查询条件有索引和排序字段有索引时,优先按照条件字段查找】 (
3)查询条件是主键,排序字段有二级索引。(InnoDB/MyISAM) Id是主键,age字段上有二级索引。先根据主键的聚簇索引结构查找结果集,然后再根据age字段在sort buffer中排序,age字段的二级索引用不上。 (4)查询条件channelId字段没有索引,排序字段age有二级索引。(InnoDB/MyISAM) 查询条件channelId字段没有索引,做全表扫描,查出符合条件的结果集,然后根据age字段值在sort buffer中排序,age字段上的二级索引根本无用武之地。
【如果查询条件上没有索引,非做全表扫描不可,除非有limit】。【索引主要作用于查询,对排序几乎用不上,除非排序字段就是查询条件,这样沾查询字段的光,查询结果直接有序,
就不用再排序了】

  【sort buffer所占内存空间】

  Sort buffer所使用的内存区域也就是我们通过 sort_buffer_size 系统变量所设置的排序区。这个排序区是每个 Thread 独享的,所以说可能在同一时刻在 MySQL 中可能
存在多个 sort buffer 内存区域。filesort排序,如果 sort_buffer 和 read_rnd_buffer 不够大,排序会用到磁盘文件,每部分在sort buffer中排序后,将各部门合并
成结果集。用到磁盘文件时,explain执行计划可能除了filesort还出现Filesort_on_disk。
<1> sort_buffer_size 排序缓冲空间大小。每个排序线程分配的缓冲区的大小。对于比较小的记录集直接在内存的sort buffer中进行排序即可完成,如果空间不够大可能用到磁盘文件,所以适当加大sort_b
uffer_size,使排序只在内存中进行,可以加快ORDER BY或GROUP BY操作。
<2>Sort_merge_passes 如果sort_buffer_size不够大,用到磁盘文件排序,大排序分成多个子排序,每部分在sort buffer中排序后,将各部门合并成结果集。有多少个子排序这个值就是多少。这个变量也
是thread级别的。

  【sort buffer排序算法实现】

MySQL中filesort 的实现算法实际上是有两种的:
一种:首先根据相应的条件取出相应的排序字段和可以直接定位行数据的行指针信息,然后在 sort buffer 中对条件进行排序排好序之后利用指针取出数据行中的请求数据,然后返回
给客户端。以前老版本只支持这种,之后版本也都支持; 一种:第一种算法基础上优化后的算法。Mysql4.1以后开始支持。一次性取出排序的条件字段和其他所有请求的字段,将不用于排序的字段存放在一块内存中,然后在 sort buffer 中
对条件字段进行排序,排好序后利用行指针将在内存中的数据进行匹配合并结果集,然后将排好序的数据返回给客户端。减少第一次算法中需要两次访问表数据的 IO 操作,将两次变成了
一次,但相应也会耗用更多的 sort buffer 空间。
<3>max_length_for_sort_data MySQL 主要通过比较我们所设定的系统参数 max_length_for_sort_data 的大小和 Query 语句所取出的字段类型大小总和来判定需要使用哪一种排序算法。如果 max_length_fo
r_sort_data 更大,则使用第二种优化后的算法,反之使用第一种算法。所以如果希望 ORDER BY 操作的效率尽可能的高,一定要注意 max_length_for_sort_data 参数的设置。
曾经就有同事的数据库出现大量的排序等待,造成系统负载很高,而且响应时间变得很长,最后查出正是因为 MySQL 使用了传统的第一种排序算法而导致,在加大了 max_length_for_
sort_data 参数值之后,系统负载马上得到了大的缓解,响应也快了很多。

  【大字段排序】

<4>max_sort_length
  默认1024.当排序BLOB或TEXT值时使用的字节数。只使用每个值的前max_sort_length字节;其它的被忽略。
【sort buffer】
MySQL 用此内存区域进行排序操作(filesort),完成客户端的排序请求。当我们设置的排序区缓存大小无法满足排序实际所需内存的时候,MySQL 会将数据写入磁盘文件来完成排序。
由于磁盘和内存的读写性能完全不在一个数量级,所以sort_buffer_size参数对排序操作的性能影响绝对不可小视。 Sort buffer在server层,跟存储引擎没有关系,如果从存储引擎返回的数据有序则不排序,否则在sort buffer中排序。

  【order by优化】

1.加大max_length_for_sort_data参数值,尽量使mysql使用第二种排序算法,这样可以减少大量的IO操作。当需要取出的所有数据长度小于这个参数的值的时候,mysql将采用第
二中改进的排序算法,否则,使用第一种算法,所以只要内存充足就可以设置足够大的值来让mysql采用改进的排序算法;
2. 去掉不必要的返回字段,很容易从上一点知道原因; 3. 增大sort_buffer_size参数的值,当mysql对条件字段进行排序时,如果需要排序字段的总长度大于该参数的值的时候,mysql就会对需要排序的字段使用临时表进行分段,这样也
会有性能的消耗。
4.加大max_length_for_sort_data参数的设置。当需要取出的所有数据长度小于这个参数的值的时候,mysql将采用第二中改进的排序算法。 同时,去掉不必要的返回字段,这可以
减少数据长度大于max_length_for_sort_data的可能;

 

4、group by 实现原理

  分组是在排序之后的。
  GROUP BY 实际上也同样会进行排序操作,而且与 ORDER BY 相比,GROUP BY 主要只是多了排序之后的分组操作。当然,如果在分组的时候还使用了其他的一些聚合函数,那么还需要一些聚合函数的计算。所以,在GROUP BY 的实现过程中,与 ORDER BY 一样也可以利用到索引。

  

SELECT * FROM table1 group by channelId;

mysql> SELECT id,groupId,userId,channelId FROM table1 group by channelId;
+----+---------+--------+-----------+
| id | groupId | userId | channelId |
+----+---------+--------+-----------+
|  2 |       3 |     12 |         1 |
| 20 |       2 |     10 |         2 |
| 30 |       3 |     23 |         3 |
|  3 |       4 |      3 |        12 |
|  4 |       4 |      3 |        13 |
+----+---------+--------+-----------+
mysql> explain SELECT id,groupId,userId,channelId FROM table1 group by channelId;
+----+-------------+--------+-------+---------------+--------+---------+------+------+-----------------
| id | select_type | table  | type  | possible_keys | key    | key_len | ref  | rows | Extra                                        
+----+-------------+--------+-------+---------------+--------+---------+------+------+-----------------
|  1 | SIMPLE      | table1 | index | NULL          | index1 | 12      | NULL |    5 | Using index; Using temporary; Using filesort |
+----+-------------+--------+-------+---------------+--------+---------+------+------+-----------------

在 MySQL 中,GROUP BY 的实现同样有多种(三种)方式,其中有两种方式会利用现有的索引信息来完成 GROUP BY,另外一种为完全无法使用索引的场景下使用。下面我们分别针对
这三种实现方式做一个分析。
【松散(Loose)索引扫描实现 GROUP BY】 所谓的松散索引扫描,就是mysql不需要扫描所有满足条件的索引键即可完成group by操作,下面通过一个简单的实例来分析一下这个过程。 CREATE TABLE `table1` ( `id` int(
11) NOT NULL AUTO_INCREMENT, `name` varchar(4000) DEFAULT NULL, `groupId` int(11) NOT NULL, `userId` int(11) NOT NULL, `channelId` int(11) NOT NULL, PRIMARY KEY (`id`), KEY `index1` (`groupId`,`userId`,`channelId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 【因为数据太少的话所有数据都在一个数据页,查找直接就查数据页仅有的一页去了,不能代表典型,所以这里用name字段4000个单位varchar来充量,插入5条记录,
形成一个小型的索引结构,数据如下图】
Mysql>SELECT userId,max(channelId) FROM table1 where groupId>1 group by groupId,userId ;

理解:
  group by的特性:有多少个goupId和userId组合就有多少group组,
  因为索引本身是按goupId,userId,channelId排序的,所以检查到id=3这个记录对应索引<4,3,…>时不做任何操作,向下检查检查到又是,<4,3,…>,不做任何操作,检查下一个
索引变成比如<5,1,..>跟<4,3,…>不一样了,将最后一个<4,3,…>对应的channelId记录到结果集即可,不用每个<4,3,…>都将channelId取出来做比较。 Mysql> explain SELECT groupId ,max(channelId) FROM table1 where groupId<10 group by groupId,userId;
执行计划的 Extra中显示“Using index for group-by”,告诉我们,MySQL Query Optimizer 通过使用松散索引扫描来实现了GROUP BY 操作。
要利用到松散索引扫描实现 GROUP BY,需要至少满足以下几个条件:
◆GROUP BY 条件字段必须在同一个索引中最前面的连续位置;
◆在使用GROUP BY 的同时,只能使用 MAX 和 MIN 这两个聚合函数(因为利用的是索引自身的有序顺序,要不就最前要不最后);
◆如果引用到了该索引中 GROUP BY 条件之外的字段条件的时候,必须以常量形式存在;

为什么松散索引扫描的效率会很高?
因为在没有WHERE子句,也就是必须经过全索引扫描的时候, 松散索引扫描需要读取的键值数量与分组的组数量一样多,也就是说比实际存在的键值数目要少很多。而在WHERE子句包含范围判断式或者等值表达式的时候, 松散索引扫描查找满足范围条件的每个组的第1个关键字,并且再次读取尽可能最少数量的关键字。
【使用紧凑(Tight)索引扫描实现 GROUP BY】
紧凑索引扫描实现 GROUP BY 和松散索引扫描的区别主要在于他需要在扫描索引的时候,读取所有满足条件的索引键,然后再根据读取恶的数据来完成 GROUP BY 操作得到相应结果。
mysql> explain select userId,max(channelId) FROM table1 where groupId=2 group by userId;
+----+-------------+--------+------+---------------+--------+---------+-------+------+-----------------
| id | select_type | table  | type | possible_keys | key    | key_len | ref   | rows | Extra                    
+----+-------------+--------+------+---------------+--------+---------+-------+------+-----------------
|  1 | SIMPLE      | table1 | ref  | index1        | index1 | 4       | const |    2 | Using where; Using index |
这时GROUP BY 操作也是通过索引完成的,只不过是需要访问WHERE条件groupId=4的全部索引键信息之后才能得出结果。

【使用临时表实现 GROUP BY】

松散索引扫描

  MySQL 在进行 GROUP BY 操作的时候要想利用所有,必须满足 GROUP BY 的字段必须同时存放于同一个索引中,且该索引是一个有序索引(如 Hash 索引就不能满足要求)。而且,并不只是如此,是否能够利用索引来实现 GROUP BY 还与使用的聚合函数也有关系。当 MySQL Query Optimizer 无法找到合适的索引可以利用的时候,就不得不先读取需要的数据,然后通过临时表来完成 GROUP BY 操作。
mysql> explain select userId,max(channelId) FROM table1 where groupId>2 group by userId;
+----+-------------+--------+-------+---------------+--------+---------+------+------+-----------------
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra
+----+-------------+--------+-------+---------------+--------+---------+------+------+-----------------| 1 | SIMPLE | table1 | range | index1 | index1 | 4 | NULL | 4 | Using where; Using index; Using temporary; Using filesort |
这次的执行计划非常明显的告诉我们 MySQL 通过索引找到了我们需要的数据,然后创建了临时表,又进行了排序操作,才得到我们需要的 GROUP BY 结果。

 

 

5、distinct 实现原理

  distinct是在分组之后的。

DISTINCT 实际上和 GROUP BY 操作的实现非常相似,只不过是在 GROUP BY 之后的每组中只取出一条记录而已。所以,DISTINCT 的实现和 GROUP BY 的实现也基本差不多,
没有太大的区别。同样可以通过松散索引扫描或者是紧凑索引扫描来实现,当然,在无法仅仅使用索引即能完成 DISTINCT 的时候,MySQL 只能通过临时表来完成。但是,和 GROUP
BY 有一点差别的是,DISTINCT并不需要进行排序。也就是说,在仅仅只是 DISTINCT 操作的 Query 如果无法仅仅利用索引完成操作的时候,MySQL 会利用临时表来做一次数据的
“缓存”,但是不会对临时表中的数据进行 filesort 操作。
1.通过松散索引扫描完成 DISTINCT : sky@localhost : example 11:03:41> EXPLAIN SELECT DISTINCT group_id -> FROM group_messageG *************************** 1. row *************************** id: 1 SELECT_type: SIMPLE table: group_message type: range possible_keys: NULL key: idx_gid_uid_gc key_len: 4 ref: NULL rows: 10 Extra: Using index for group-by 1 row in set (0.00 sec) 执行计划中的 Extra 信息为“Using index for group-by”,这代表什么意思?为什么我没有进行 GROUP BY 操作的时候,执行计划中会告诉我这里通过索引进行了 GROUP BY 呢?
其实这就是于 DISTINCT 的实现原理相关的,在实现 DISTINCT的过程中,同样也是需要分组的,然后再从每组数据中取出一条返回给客户端。而这里的 Extra 信息就告诉我们,MySQ
L 利用松散索引扫描就完成了整个操作。

2.通过紧凑索引扫描完成distinct: sky@localhost : example 11:03:53> EXPLAIN SELECT DISTINCT user_id -> FROM group_message -> WHERE group_id = 2G *************************** 1. row *************************** id: 1 SELECT_type: SIMPLE table: group_message type: ref possible_keys: idx_gid_uid_gc key: idx_gid_uid_gc key_len: 4 ref: const rows: 4 Extra: Using WHERE; Using index 1 row in set (0.00 sec) 这里的显示和通过紧凑索引扫描实现 GROUP BY 也完全一样。实际上,这个 Query 的实现过程中,MySQL 会让存储引擎扫描 group_id = 2 的所有索引键,得出所有的 user_id,
然后利用索引的已排序特性,每更换一个 user_id 的索引键值的时候保留一条信息,即可在扫描完所有 gruop_id = 2 的索引键的时候完成整个 DISTINCT 操作。 3.无法单独使用索引时的DISTINCT : sky@localhost : example 11:04:40> EXPLAIN SELECT DISTINCT user_id -> FROM group_message -> WHERE group_id > 1 AND group_id < 10G *************************** 1. row *************************** id: 1 SELECT_type: SIMPLE table: group_message type: range possible_keys: idx_gid_uid_gc key: idx_gid_uid_gc key_len: 4 ref: NULL rows: 32 Extra: Using WHERE; Using index; Using temporary 1 row in set (0.00 sec) 当 MySQL 无法仅仅依赖索引即可完成 DISTINCT 操作的时候,就不得不使用临时表来进行相应的操作了。但是我们可以看到,在 MySQL 利用临时表来完成 DISTINCT 的时候,
和处理 GROUP BY 有一点区别,就是少了 filesort。实际上,在 MySQL 的分组算法中,并不一定非要排序才能完成分组操作的,这一点在上面的 GROUP BY 优化小技巧中我
已经提到过了。实际上这里 MySQL 正是在没有排序的情况下实现分组最后完成 DISTINCT 操作的,所以少了 filesort 这个排序操作。
4.最后再和 GROUP BY 结合试试看: sky@localhost : example 11:05:06> EXPLAIN SELECT DISTINCT max(user_id) -> FROM group_message -> WHERE group_id > 1 AND group_id < 10 -> GROUP BY group_idG *************************** 1. row *************************** id: 1 SELECT_type: SIMPLE table: group_message type: range possible_keys: idx_gid_uid_gc key: idx_gid_uid_gc key_len: 4 ref: NULL rows: 32 Extra: Using WHERE; Using index; Using temporary; Using filesort 1 row in set (0.00 sec)   最后我们再看一下这个和 GROUP BY 一起使用带有聚合函数的示例,和上面第三个示例相比,可以看到已经多了 filesort 排序操作了,正是因为我们使用了 MAX 函数的缘故。
要取得分组后的 MAX 值,又无法使用索引完成操作,只能通过排序才行了。

  

 

posted @ 2019-03-26 16:01  Bourne.D  阅读(420)  评论(0编辑  收藏  举报