热门标签 | HotTags
当前位置:  开发笔记 > 后端 > 正文

MySQLInternals-IndexMerge优化

之前搞错了,以为IndexMerge是MySQL5.6的新特性,原来不是,发现5.5也有,看了下manual,发现5.0的manual就已经存在了,可以说是一个历史悠久的优化手段了,好吧,不管怎么样,今天就拨开其神秘的面纱,看看其内部到底如何生成这种IndexMerge的计划的。这里只详细介绍Intersect

0  前言

之前搞错了,以为Index Merge是MySQL5.6的新特性,原来不是,发现5.5也有,看了下manual,发现5.0的manual就已经存在了, 可以说是一个历史悠久的优化手段了,好吧,不管怎么样,今天就拨开其神秘的面纱,看看其内部到底如何生成这种Index Merge的计划的。 这里只详细介绍Intersect操作,对于Union和Sort-Union的具体代码,还没开始研究。

 

1  Index Merge理论基础

Index Merge——索引归并,即针对一张表,同时使用多个索引进行查询,然后将各个索引查出来的结果进行进一步的操作,可以是求交 ——Intersect,也可以是求和——Union,针对union还有一种补充算法——Sort-Union,很奇怪为什么没有Sort-Intersect,按道理也是可以做的。

 

什么情况下,同时使用多个索引会有利呢?比如说WHERE条件是C1=10 AND C2 =100,但是只有分别针对C1和C2的索引,而没有(C1,C2)这种索引, 两个索引同时使用才有意义,通过两个索引都可以快速定位到一批数据,然后对这一批数据进行进一步的求交或求和操作即可,这样的效率可能比 全表扫描或者只使用其中一个索引进行扫描然后再去主索引查询要快。

 

Intersect和Union都需要使用的索引是ROR的,也就时ROWID ORDERED,即针对不同的索引扫描出来的数据必须是同时按照ROWID排序的,这里的 ROWID其实也就是InnoDB的主键(如果不定义主键,InnoDB会隐式添加ROWID列作为主键)。只有每个索引是ROR的,才能进行归并排序,你懂的。 当然你可能会有疑惑,查不记录后内部进行一次sort不一样么,何必必须要ROR呢,不错,所以有了SORT-UNION。SORT-UNION就是每个非ROR的索引 排序后再进行Merge。至于为什么没有SORT-INTERSECT,我也很是迷茫。

 

2  初始化数据

mysql>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
mysql> show create table im\G 
*************************** 1. row *************************** 
       Table: im 
Create Table: CREATE TABLE `im` ( 
  `c1` int(11) DEFAULT NULL, 
  `c2` int(11) DEFAULT NULL, 
  `c3` int(11) DEFAULT NULL, 
  KEY `c1` (`c1`,`c3`), 
  KEY `c2` (`c2`,`c1`) 
) ENGINE=InnoDB DEFAULT CHARSET=latin1 
1 row in set (0.00 sec) 
    
mysql> show create procedure fill_im1\G 
*************************** 1. row *************************** 
           Procedure: fill_im1 
            sql_mode: NO_ENGINE_SUBSTITUTION 
    Create Procedure: CREATE DEFINER=`root`@`127.0.0.1` PROCEDURE `fill_im1`(cnt int) 
begindeclarei intdefault0; repeat insert into im values(100, 50, 100); set i=i+1; until i > cntendrepeat;end
character_set_client: utf8 
collation_connection: utf8_general_ci 
  Database Collation: latin1_swedish_ci 
1 row in set (0.07 sec) 
    
mysql> show create procedure fill_im2\G 
*************************** 1. row *************************** 
           Procedure: fill_im2 
            sql_mode: NO_ENGINE_SUBSTITUTION 
    Create Procedure: CREATE DEFINER=`root`@`127.0.0.1` PROCEDURE `fill_im2`(cnt int) 
begindeclarei intdefault0; repeat insert into im values(100, 100, 50); set i=i+1; until i > cntendrepeat;end
character_set_client: utf8 
collation_connection: utf8_general_ci 
  Database Collation: latin1_swedish_ci 
1 row in set (0.00 sec) 
    
    
mysql> call fill_im1(2000) 
mysql> call fill_im2(2000) 
    
mysql> insert into im values(100,50,50); 
Query OK, 1 row affected (0.00 sec) 
mysql> insert into im values(100,50,50); 
Query OK, 1 row affected (0.00 sec) 
    
mysql> commit; 
Query OK, 0 rows affected (0.05 sec) 
    
    
mysql> select * from im where c1=100andc2 = 50andc3 = 50\G 
*************************** 1. row *************************** 
c1: 100 
c2: 50 
c3: 50 
*************************** 2. row *************************** 
c1: 100 
c2: 50 
c3: 50 
2 rows in set (0.13 sec)

 

3  执行计划

mysql>
1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> explain select * from im where c1=100and c2 =50and c3 =50\G 
***************************1. row *************************** 
           id:1
  select_type: SIMPLE 
        table: im 
         type: index_merge 
possible_keys: c1,c2 
          key: c1,c2 
      key_len:10,10
          ref: NULL 
         rows:1001
        Extra: Using intersect(c1,c2); Using where; Using index 
1rowinset(0.00sec)

 

4  代码分析

从生成数据的方法可以看出来,是专门针对查询的语句进行构造的。无论是根据(c1,c3)的索引查询还是根据(c2,c1)的索引查询, 都会查出一般的数据,即效率接近于全表扫描的一半。但是如果利用两个索引同时进行过滤,那么过滤出来的数据就很少了,也就是 结果中的两条。

 

也就是说如果单独查询各个索引,过滤效果不明显,但是如果联合两个索引进行MERGE过滤,那么效果可能很明显,这里所说的过滤,用更 专业的词来说是选择因子——selectivity。而计划的选择时代价的计算,便是计算这个选择因子。如果综合多个索引,导致选择因子很小,从而 达到索引merge出来的结果集很小的话,那么计划就更倾向于Index Merge,反之则不然。

 

下面是选择子计算的代码:

static
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
staticdouble ror_scan_selectivity(constROR_INTERSECT_INFO *info,  
                                   constROR_SCAN_INFO *scan) 
  double selectivity_mult=1.0; 
  constTABLE *cOnsttable= info->param->table; 
  constKEY_PART_INFO *constkey_part= table->key_info[scan->keynr].key_part; 
  /** 
    key values tuple, used to store both min_range.key and 
    max_range.key. This function is only called for equality ranges; 
    open ranges (e.g. "min_value
    rowid ordered retrieval, so in this function we know that 
    min_range.key == max_range.key 
  */
  uchar key_val[MAX_KEY_LENGTH+MAX_FIELD_WIDTH]; 
  uchar *key_ptr= key_val; 
  SEL_ARG *sel_arg, *tuple_arg= NULL; 
  key_part_map keypart_map=0; 
  bool cur_covered; 
  bool prev_covered= test(bitmap_is_set(&info->covered_fields, 
                                        key_part->fieldnr-1)); 
  key_range min_range; 
  key_range max_range; 
  min_range.key= key_val; 
  min_range.flag= HA_READ_KEY_EXACT; 
  max_range.key= key_val; 
  max_range.flag= HA_READ_AFTER_KEY; 
  ha_rows prev_records= table->file->stats.records; 
  DBUG_ENTER("ror_scan_selectivity"); 
    
  for(sel_arg= scan->sel_arg; sel_arg; 
       sel_arg= sel_arg->next_key_part) 
  { 
    DBUG_PRINT("info",("sel_arg step")); 
    cur_covered= test(bitmap_is_set(&info->covered_fields, 
                                    key_part[sel_arg->part].fieldnr-1)); 
    if(cur_covered != prev_covered) 
    { 
      /* create (part1val, ..., part{n-1}val) tuple. */
      bool is_null_range=false; 
      ha_rows records; 
      if(!tuple_arg) 
      { 
        tuple_arg= scan->sel_arg; 
        /* Here we use the length of the first key part */
        tuple_arg->store_min(key_part[0].store_length, &key_ptr,0); 
        is_null_range|= tuple_arg->is_null_interval(); 
        keypart_map=1; 
      } 
      while(tuple_arg->next_key_part != sel_arg) 
      { 
        tuple_arg= tuple_arg->next_key_part; 
        tuple_arg->store_min(key_part[tuple_arg->part].store_length, 
                             &key_ptr,0); 
        is_null_range|= tuple_arg->is_null_interval(); 
        keypart_map= (keypart_map <<1) |1; 
      } 
      min_range.length= max_range.length= (size_t) (key_ptr - key_val); 
      min_range.keypart_map= max_range.keypart_map= keypart_map; 
    
      /*  
        Get the number of rows in this range. This is done by calling 
        records_in_range() unless all these are true: 
          1) The user has requested that index statistics should be used 
             for equality ranges to avoid the incurred overhead of  
             index dives in records_in_range() 
          2) The range is not on the form "x IS NULL". The reason is 
             that the number of rows with this value are likely to be 
             very different than the values in the index statistics 
          3) Index statistics is available. 
        @see key_val 
      */
      if(!info->param->use_index_statistics ||       // (1) 
          is_null_range ||                            // (2) 
          !(records= table->key_info[scan->keynr]. 
                     rec_per_key[tuple_arg->part]))   // (3) 
      { 
        DBUG_EXECUTE_IF("crash_records_in_range", DBUG_SUICIDE();); 
        DBUG_ASSERT(min_range.length >0); 
        records= (table->file-> 
                  records_in_range(scan->keynr, &min_range, &max_range)); 
      } 
      if(cur_covered) 
      { 
        /* uncovered -> covered */
        double tmp= rows2double(records)/rows2double(prev_records); 
        DBUG_PRINT("info", ("Selectivity multiplier: %g", tmp)); 
        selectivity_mult *= tmp; 
        prev_records= HA_POS_ERROR; 
      } 
      else
      { 
        /* covered -> uncovered */
        prev_records= records; 
      } 
    } 
    prev_covered= cur_covered; 
  } 
  if(!prev_covered) 
  { 
    double tmp= rows2double(table->quick_rows[scan->keynr]) / 
                rows2double(prev_records); 
    DBUG_PRINT("info", ("Selectivity multiplier: %g", tmp)); 
    selectivity_mult *= tmp; 
  } 
  // Todo: This assert fires in PB sysqa RQG tests. 
  // DBUG_ASSERT(selectivity_mult <= 1.0); 
  DBUG_PRINT("info", ("Returning multiplier: %g", selectivity_mult)); 
  DBUG_RETURN(selectivity_mult); 
    
    
     
刚看到这段代码时,确实有点犯懵,代码的注释给了很大的帮助: 
    
/* 
  Get selectivity of adding a ROR scan to the ROR-intersection. 
    
  SYNOPSIS 
    ror_scan_selectivity() 
      info  ROR-interection, an intersection of ROR index scans  
      scan  ROR scan that may or may not improve the selectivity 
            of 'info' 
          
  NOTES 
    Suppose we have conditions on several keys 
    cOnd=k_11=c_11 AND k_12=c_12 AND ...  // key_parts of first key in 'info' 
         k_21=c_21 AND k_22=c_22 AND ...  // key_parts of second key in 'info' 
          ... 
         k_n1=c_n1 AND k_n3=c_n3 AND ...  (1) //key_parts of 'scan' 
    
    where k_ij may be the same as any k_pq (i.e. keys may have common parts). 
    
    Note that for ROR retrieval, only equality conditions are usable so there 
    are no open ranges (e.g., k_ij > c_ij) in 'scan' or 'info' 
    
    A full row is retrieved if entire condition holds. 
    
    The recursive procedure for finding P(cond) is as follows: 
    
    First step: 
    Pick 1st part of 1st key and break conjunction (1) into two parts: 
      cOnd= (k_11=c_11 AND R) 
    
    Here R may still contain condition(s) equivalent to k_11=c_11. 
    Nevertheless, the following holds: 
    
      P(k_11=c_11 AND R) = P(k_11=c_11) * P(R | k_11=c_11). 
    
    Mark k_11 as fixed field (and satisfied condition) F, save P(F), 
    save R to be cond and proceed to recursion step. 
    
    Recursion step: 
    We have a set of fixed fields/satisfied conditions) F, probability P(F), 
    and remaining conjunction R 
    Pick next key part on current key and its condition "k_ij=c_ij". 
    We will add "k_ij=c_ij" into F and update P(F). 
    Lets denote k_ij as t,  R = t AND R1, where R1 may still contain t. Then 
    
     P((t AND R1)|F) = P(t|F) * P(R1|t|F) = P(t|F) * P(R1|(t AND F)) (2) 
    
    (where '|' mean conditional probability, not "or") 
    
    Consider the first multiplier in (2). One of the following holds: 
    a) F contains condition on field used in t (i.e. t AND F = F). 
      Then P(t|F) = 1 
    
    b) F doesn't contain condition on field used in t. Then F and t are 
     considered independent. 
    
     P(t|F) = P(t|(fields_before_t_in_key AND other_fields)) = 
          = P(t|fields_before_t_in_key). 
    
     P(t|fields_before_t_in_key) = #records(fields_before_t_in_key) / 
                                   #records(fields_before_t_in_key, t) 
    
    The second multiplier is calculated by applying this step recursively. 
    
  IMPLEMENTATION 
    This function calculates the result of application of the "recursion step" 
    described above for all fixed key members of a single key, accumulating set 
    of covered fields, selectivity, etc. 
    
    The calculation is conducted as follows: 
    Lets denote #records(keypart1, ... keypartK) as n_k. We need to calculate 
    
     n_{k1}      n_{k2} 
    --------- * ---------  * .... (3) 
     n_{k1-1}    n_{k2-1} 
    
    where k1,k2,... are key parts which fields were not yet marked as fixed 
    ( this is result of application of option b) of the recursion step for 
      parts of a single key). 
    Since it is reasonable to expect that most of the fields are not marked 
    as fixed, we calculate (3) as 
    
                                  n_{i1}      n_{i2} 
    (3) = n_{max_key_part}  / (   --------- * ---------  * ....  ) 
                                  n_{i1-1}    n_{i2-1} 
    
    where i1,i2, .. are key parts that were already marked as fixed. 
    
    In order to minimize number of expensive records_in_range calls we 
    group and reduce adjacent fractions. Note that on the optimizer's 
    request, index statistics may be used instead of records_in_range 
    @see RANGE_OPT_PARAM::use_index_statistics. 
    
  RETURN 
    Selectivity of given ROR scan, a number between 0 and 1. 1 means that 
    adding 'scan' to the intersection does not improve the selectivity. 
*/

 

注释想说明的就是选择因子的概率如何进行计算,其实就是不同INDEX之间差异性的索引列会引起选择因子不断变小,即 Index之间差异性越大,过滤的记录就越多,选择出来的数据集就会越少。INDEX的差异性就是INdex之间索引列列是否重复出现在 不同索引之间,两个INDEX约相似,那么MERGE的结果集越大。具体的实现大家自己看看吧,明白了原理,实现都是浮云了。

 

BTW, 5.6的Optimizer trace十分好用,对于想要跟踪Optimizer内部的同学来说,可以先把详细的计划生成流程通过Optimizer trace 打印出来,对照优化流程,就能更好的定位到代码。


推荐阅读
  • Spring Boot 实战(一):基础的CRUD操作详解
    在《Spring Boot 实战(一)》中,详细介绍了基础的CRUD操作,涵盖创建、读取、更新和删除等核心功能,适合初学者快速掌握Spring Boot框架的应用开发技巧。 ... [详细]
  • 深入解析:Explain命令的应用与字段详解
    深入解析:Explain命令的应用与字段详解 ... [详细]
  • 如何运用蒙特卡洛方法计算NPV:计算机专业毕业设计遇到难题怎么办?
    许多计算机科学专业的学生在大学期间都会遇到这样的困扰:课堂上教授的内容往往偏向理论,实际应用的知识点讲解得较为浅显和概括,导致在进行毕业设计时,如运用蒙特卡洛方法计算净现值(NPV)等复杂问题时感到无从下手。本文旨在探讨如何通过深入理解和实践蒙特卡洛模拟技术,解决这类计算难题,为学生的毕业设计提供实用指导。 ... [详细]
  • 从无到有,构建个人专属的操作系统解决方案
    操作系统(OS)被誉为程序员的三大浪漫之一,常被比喻为计算机的灵魂、大脑、内核和基石,其重要性不言而喻。本文将详细介绍如何从零开始构建个人专属的操作系统解决方案,涵盖从需求分析到系统设计、开发与测试的全过程,帮助读者深入理解操作系统的本质与实现方法。 ... [详细]
  • 在Ubuntu系统中,由于预装了MySQL,因此无需额外安装。通过命令行登录MySQL时,可使用 `mysql -u root -p` 命令,并按提示输入密码。常见问题包括:1. 错误 1045 (28000):访问被拒绝,这通常是由于用户名或密码错误导致。为确保顺利连接,建议检查MySQL服务是否已启动,并确认用户名和密码的正确性。此外,还可以通过配置文件调整权限设置,以增强安全性。 ... [详细]
  • 如何使用Python高效绘制矩形图形
    本文详细介绍了如何利用Python的Turtle库高效绘制矩形图形,适合初学者快速上手。通过具体示例代码,帮助读者理解Turtle库的基本绘图方法和技巧,同时探讨了在不同应用场景中绘制矩形的实际操作,为后续复杂图形的绘制打下坚实基础。 ... [详细]
  • 智能制造数据综合分析与应用解决方案
    在智能制造领域,生产数据通过先进的采集设备收集,并利用时序数据库或关系型数据库进行高效存储。这些数据经过处理后,通过可视化数据大屏呈现,为生产车间、生产控制中心以及管理层提供实时、精准的信息支持,助力不同应用场景下的决策优化和效率提升。 ... [详细]
  • 在主从复制架构中,Bingo_MySQL 同步工具的应用与优化具有重要意义。为确保高效同步,建议使用相同或兼容的 MySQL 版本,并确保两台服务器位于同一局域网内,且网络连接畅通无阻。若无法 ping 通,请检查 IP 配置及防火墙设置,以保证网络连通性。此外,合理的配置参数和定期维护也是提升同步性能的关键因素。 ... [详细]
  • HBase在金融大数据迁移中的应用与挑战
    随着最后一台设备的下线,标志着超过10PB的HBase数据迁移项目顺利完成。目前,新的集群已在新机房稳定运行超过两个月,监控数据显示,新集群的查询响应时间显著降低,系统稳定性大幅提升。此外,数据消费的波动也变得更加平滑,整体性能得到了显著优化。 ... [详细]
  • 本文深入探讨了数据库性能优化与管理策略,通过实例分析和理论研究,详细阐述了如何有效提升数据库系统的响应速度和处理能力。文章首先介绍了数据库性能优化的基本原则和常用技术,包括索引优化、查询优化和存储管理等。接着,结合实际应用场景,讨论了如何利用容器化技术(如Docker)来部署和管理数据库,以提高系统的可扩展性和稳定性。最后,文章还提供了具体的配置示例和最佳实践,帮助读者在实际工作中更好地应用这些策略。 ... [详细]
  • 从用户转型为开发者:一场思维升级的旅程 | 专访 StarRocks Committer 周威
    从用户转变为开发者,不仅是一次角色的转换,更是一场深刻的思维升级之旅。本次专访中,StarRocks Committer 周威分享了他如何在这一过程中逐步提升技术能力与思维方式,为开源社区贡献自己的力量。 ... [详细]
  • 本文详细解析了 MySQL 5.7.20 版本中二进制日志(binlog)崩溃恢复机制的工作流程。假设使用 InnoDB 存储引擎,并且启用了 `sync_binlog=1` 配置,文章深入探讨了在系统崩溃后如何通过 binlog 进行数据恢复,确保数据的一致性和完整性。 ... [详细]
  • Java 零基础入门:SQL Server 学习笔记(第21篇)
    Java 零基础入门:SQL Server 学习笔记(第21篇) ... [详细]
  • MySQL性能优化与调参指南【数据库管理】
    本文详细探讨了MySQL数据库的性能优化与参数调整技巧,旨在帮助数据库管理员和开发人员提升系统的运行效率。内容涵盖索引优化、查询优化、配置参数调整等方面,结合实际案例进行深入分析,提供实用的操作建议。此外,还介绍了常见的性能监控工具和方法,助力读者全面掌握MySQL性能优化的核心技能。 ... [详细]
  • 2019年后蚂蚁集团与拼多多面试经验详述与深度剖析
    2019年后蚂蚁集团与拼多多面试经验详述与深度剖析 ... [详细]
author-avatar
mobiledu2502856483
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有