MySQL Join算法与调优白皮书(二)

Index Nested-Loop Join
 
(接上篇)因为访问的是辅助索引,若是查询须要访问汇集索引上的列,那么必要须要进行回表取数据,看似每条记录只是多了一次回表操做,但这才是INLJ 算法最大的弊端。首先,辅助索引的index lookup是比较随机I/O访问操做。其次,根据index lookup再进行回表又是一个随机的I/O操做。因此说,INLJ最大的弊端是其可能须要大量的离散操做,这在SSD出现以前是最大的瓶颈。而即便SSD的出现大幅提高了随机的访问性能,可是对比顺序I/O,其仍是慢了不少,依然不在一个数量级上。例以下面的这个 SQL语句:
 
SELECT
  COUNT(*)
FROM
  part,
  lineitem
WHERE
  l_partkey = p_partkey
      AND p_retailprice > 2050
AND l_discount > 0.04;
 
其中p_partkey是表part的主键,l_partkey是表lineitem的一个辅助索引,因为表part数据较小,所以做为外表(驱动表)。可是内表Join完成后还须要判断条件l_discount > 0.04,这个在汇集索引上,故须要回表进行读取。根据explain获得上述SQL的执行计划以下图所示:

Block Nested-Loop Join
 
算法说明
 
在有索引的状况下,MySQL会尝试去使用Index Nested-Loop Join算法,在有些状况下,可能Join的列就是没有索引,那么这时MySQL的选择绝对不会是最早介绍的Simple Nested-Loop Join算法,由于那个算法太粗暴,不忍直视。数据量大些的复杂SQL估计几年均可能跑不出结果,若是你不信,那就是too young too simple。或者Inside君能够给你些SQL跑跑看。
 
Simple Nested-Loop Join算法的缺点在于其对于内表的扫描次数太多,从而致使扫描的记录太过庞大。Block Nested-Loop Join算法较Simple Nested-Loop Join的改进就在于能够减小内表的扫描次数,甚至能够和Hash Join算法同样,仅需扫描内表一次。
 
接着Inside君带你来看看Block Nested-Loop Join算法的伪代码:
 
For each tuple r in R do
  store used columns as p from R in join buffer
  For each tuple s in S do
    If p and s satisfy the join condition
      Then output the tuple
 
能够看到相比Simple Nested-Loop Join算法,Block Nested-LoopJoin算法仅多了一个所谓的Join Buffer,然为何这样就能减小内表的扫描次数呢?下图相比更好地解释了Block Nested-Loop Join算法的运行过程:

能够看到Join Buffer用以缓存连接须要的列,而后以Join Buffer批量的形式和内表中的数据进行连接比较。就上图来看,记录r1,r2 … rT的连接仅需扫内表一次,若是join buffer能够缓存全部的外表列,那么连接仅需扫描内外表各一次,从而大幅提高Join的性能。
 
Join Buffer
 
变量join_buffer_size
 
从上一节中能够发现Join Buffer是用来减小内表扫描次数的一种优化,但Join Buffer又没那么简单,在上一节中Inside君故意忽略了一些实现。
 
首先变量join_buffer_size用来控制Join Buffer的大小,调大后能够避免屡次的内表扫描,从而提升性能。也就是说,当MySQL的Join有使用到Block Nested-Loop Join,那么调大变量join_buffer_size才是有意义的。而前面的Index Nested-Loop Join若是仅使用索引进行Join,那么调大这个变量则毫无心义。
 
变量join_buffer_size的默认值是256K,显然对于稍复杂的SQL是不够用的。好在这个是会话级别的变量,能够在执行前进行扩展。Inside君建议在会话级别进行设置,而不是全局设置,由于很难给一个通用值去衡量。另外,这个内存是会话级别分配的,若是设置很差容易致使因没法分配内存而致使的宕机问题。
 
须要特别注意的是,变量join_buffer_size的最大值在MySQL 5.1.22版本前是4G-1,而以后的版本才能在64位操做系统下申请大于4G的Join Buffer空间。
 
Join Buffer缓存的对象
 
Join Buffer缓存的对象是什么,这个问题至关关键和重要。然在MySQL的官方手册中是这样记录的:
 
Only columns of interest to the join are  stored in the join buffer, not whole rows.
 
能够发现Join Buffer不是缓存外表的整行记录,可是columns of interest具体指的又是什么?Inside君的第一反应是Join的列。为此,Inside君又去查了下mysql internals,查询获得的说明以下所示:
 
We only store the used columns in the join buffer, not the whole rows.
 
used columns仍是很是模糊。为此,Inside君询问了好友李海翔,也是官方MySQL优化器团队的成员,他答复个人结果是:“全部参与查询的列”都会保存到Join Buffer,而不是只有Join的列。最后,Inside君调试了MySQL,在sql_join_buffer.cc文件中验证了这个结果。
 
好比下面的SQL语句,假设没有索引,须要使用到Join Buffer进行连接:
 
SELECT a.col3 FROM a,b
  WHERE a.col1 = b.col2
  AND a.col2 > …. AND b.col2 = …
 
假设上述SQL语句的外表是a,内表是b,那么存放在Join Buffer中的列是全部参与查询的列,在这里就是(a.col1,a.col2,a.col3)。
 
经过上面的介绍,咱们如今能够获得内表的扫描次数为:
 
Scaninner_table = (Rn * used_column_size) / join_buffer_size + 1
 
对于有经验的DBA就能够预估须要分配的Join Buffer大小,而后尽可能使得内表的扫描次数尽量的少,最优的状况是只扫描内表一次。
 
Join Buffer的分配
 
须要牢记的是,Join Buffer是在Join以前就进行分配,而且每次Join就须要分配一次Join Buffer,因此假设有N张表参与Join,每张表之间经过Block Nested-Loop Join,那么总共须要分配N-1个Join Buffer,这个内存容量是须要DBA进行考量的。
 
Join Buffer可分为如下两类:
regular join buffer
incremental join buffer
 
regular join buffer是指Join Buffer缓存全部参与查询的列, 若是第一次使用Join Buffer,必然使用的是regular join buffer。
 
incremental join buffer中的Join Buffer缓存的是当前使用的列,以及以前使用Join Buffer的指针。在屡次进行Join的操做时,这样能够极大减小Join Buffer对于内存开销的需求。
 
此外,对于NULL类型的列,其实不须要存放在Join Buffer中,而对于VARCHAR类型的列,也是仅需最小的内存便可,而不是以CHAR类型在Join Buffer中保存。最后,从MySQL 5.6版本开始,对于Outer Join也可使用Join Buffer。
 
Block Nested-Loop Join总结
 
Block Nested-Loop Join极大的避免了内表的扫描次数,若是Join Buffer能够缓存外表的数据,那么内表的扫描仅需一次,这和Hash Join很是相似。可是Block Nested-Loop Join依然没有解决的是Join比较的次数,其仍然经过Join判断式进行比较。综上所述,到目前为止各Join算法的成本比较以下所示:

未完待续......
相关文章
相关标签/搜索