zoukankan      html  css  js  c++  java
  • 022:SQL优化--JOIN算法

    一. SQL优化--JOIN算法

    1.1. JOIN 写法对比

    
    (root@localhost) 17:51:15 [mytest]> select * from a;
    +------+------+
    | id   | num  |
    +------+------+
    | a    |    5 |
    | b    |   10 |
    | c    |   15 |
    | d    |   10 |
    | e    |   99 |
    +------+------+
    5 rows in set (0.00 sec)
    
    (root@localhost) 17:51:20 [mytest]> select * from b;
    +------+------+
    | id   | num  |
    +------+------+
    | b    |    5 |
    | c    |   15 |
    | d    |   20 |
    | e    |   99 |
    +------+------+
    4 rows in set (0.00 sec)
    
    --
    -- 语法一
    --
    
    (root@localhost) 17:51:23 [mytest]> select * from a,b where a.id=b.id;
    +------+------+------+------+
    | id   | num  | id   | num  |
    +------+------+------+------+
    | b    |   10 | b    |    5 |
    | c    |   15 | c    |   15 |
    | d    |   10 | d    |   20 |
    | e    |   99 | e    |   99 |
    +------+------+------+------+
    4 rows in set (0.00 sec)
    
    --
    -- 语法二
    --
    
    (root@localhost) 17:51:42 [mytest]> select * from a inner join b where a.id=b.id;
    +------+------+------+------+
    | id   | num  | id   | num  |
    +------+------+------+------+
    | b    |   10 | b    |    5 |
    | c    |   15 | c    |   15 |
    | d    |   10 | d    |   20 |
    | e    |   99 | e    |   99 |
    +------+------+------+------+
    4 rows in set (0.00 sec)
    
    --
    -- 语法三
    --
    
    (root@localhost) 17:52:42 [mytest]> select * from a  join b where a.id=b.id;
    +------+------+------+------+
    | id   | num  | id   | num  |
    +------+------+------+------+
    | b    |   10 | b    |    5 |
    | c    |   15 | c    |   15 |
    | d    |   10 | d    |   20 |
    | e    |   99 | e    |   99 |
    +------+------+------+------+
    4 rows in set (0.00 sec)
    
    (root@localhost) 17:55:31 [mytest]> explain select * from a,b where a.id=b.id;
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                              |
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    |  1 | SIMPLE      | b     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    4 |   100.00 | NULL                                               |
    |  1 | SIMPLE      | a     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    5 |    20.00 | Using where; Using Join Buffer (Block Nested Loop) |
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    2 rows in set, 1 warning (0.00 sec)
    
    (root@localhost) 17:55:48 [mytest]> explain select * from a inner join b where a.id=b.id;
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                              |
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    |  1 | SIMPLE      | b     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    4 |   100.00 | NULL                                               |
    |  1 | SIMPLE      | a     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    5 |    20.00 | Using where; Using Join Buffer (Block Nested Loop) |
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    2 rows in set, 1 warning (0.00 sec)
    
    (root@localhost) 17:56:06 [mytest]> explain select * from a  join b where a.id=b.id;
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                              |
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    |  1 | SIMPLE      | b     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    4 |   100.00 | NULL                                               |
    |  1 | SIMPLE      | a     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    5 |    20.00 | Using where; Using Join Buffer (Block Nested Loop) |
    +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
    2 rows in set, 1 warning (0.00 sec)
    

    通过上述的EXPLAIN可以得知,三种JOIN 语法在执行性能效果上都是一样的。

    2. JOIN的成本

    • 两张表的JOIN的过程:

    上图的Fetch阶段是指当内表关联的列辅助索引时,但是需要访问表中的数据,那么这时就需要再访问主键索引才能得到数据的过程,不论表的存储引擎是InnoDB还是MyISAM,这都是无法避免的,只是MyISAM回表速度要快点,因为其辅助索引存放的就是指向记录的指针,而InnoDB索引组织表,需要再次通过索引查找才能定位数据

    Fetch阶段也不是必须存在的,如果是聚集索引链接,那么直接就能得到数据,无需回表,也就没有Fetch这个阶段。另外,上述给出了两张表之间的Join过程,多张表的Join就是继续上述这个过程。

    • 回表:在找到二级索引后,得到对应的主键,再回到聚集索引中找主键对应的记录

    接着计算两张表Join的成本,这里有下列几种概念:

    外表的扫描次数,记为O。通常外表的扫描次数都是1,即Join时扫描一次驱动表的数据即可Join
    内表的扫描次数,记为I。根据不同Join算法,内表的扫描次数不同
    读取表的记录数,记为R。根据不同Join算法,读取记录的数量可能不同
    Join的比较次数,记为M。根据不同Join算法,比较次数不同
    回表的读取记录的数,记为F。若Join的是辅助索引,可能需要回表取得最终的数据
    

    评判Join算法是否优劣,就是查看上述这些操作的开销是否比较小。当然还要考虑I/O的访问方式顺序还是随机,总之Join的调优也是门艺术,并非想象的那么简单。

    3. JOIN算法

    • nsted_loop join
      • simple nested-loop join
      • index nested-loop join
      • block nested-loop join
    • classic Hash Join
      • Only support in MariaDB
    • bached key access join
      • from MySQL 5.6
      • from MariaDB 5.5

    3.1. simple nested loop join

    • simple nested_loog join外表(驱动表)中的每一条记录与内表中的记录进行判断

    • 其算法解析如下:

    For each row r in R do  						-- 扫描R表
        Foreach row s in S do 						-- 扫描S表
            If r and s satisfy the join condition 	-- 如果r和s满足join条件
                    Then output the tuple 			-- 那就输出结果集
    
    • 假设外表的记录数为R,内表的记录数位S
    1. R 表,该表只扫描了一次;
    2. S 表,该表扫面了count(R) 次;
    3. 该方法相当于是一个笛卡尔积,实际上数据库不会使用该算法;
    4. 根据上面对于JOIN算法的评判标准来看,SNLJ的开销如下表所示:

    3.2. index nested loop join

    • index nested_loop join 由于内表上有索引,将外表扫描一次,内表是通过查找内表相应的索引的方式,和外表的记录进行匹配。
    For each row r in R do              -- 扫描R表
        lookup s in S index             -- 查询S表的索引(固定3~4次IO,B+树高度)
            if found s == r             -- 如果 r匹配了索引s
                Then output the tuple   -- 输出结果集
    
    1. S 表上有索引
    2. 扫描R表,将R表的记录和S表中的索引进行匹配
    3. R表上可以没有索引
    4. 优化器倾向使用记录数少的表作为外表(又称驱动表)
    5. 如果数据量大,index nested loop join的成本也是高的,尤其是在二级索引的情况下,需要大量的回表操作
    6. 因为索引查询的成本是比较固定的,故优化器都倾向于使用记录数少的表作为外表。故index nested_loop join的算法成本如下表所示:

    上表Smatch表示通过索引找到匹配的记录数量。通过索引可以大幅降低内表的Join的比较次数,每次比较1条外表的记录,其实就是一次indexlookup(索引查找),而每次index lookup的成本就是树的高度,即IndexHeight

    index nested_loop join的算法的效率能否达到用户的预期呢?

    其实如果是通过表的主键索引进行Join,即使是大数据量的情况下,index nested_loop join的效率亦是相当不错的。因为索引查找的开销非常小,并且访问模式也是顺序的(假设大多数聚集索引的访问都是比较顺序的)。

    大部分人诟病MySQL的index nested_loop join慢,主要是因为在进行Join的时候可能用到的索引并不主键的聚集索引,而是辅助索引,这时index nested_loop join的过程又需要多一步Fetch的过程,而且这个过程开销会相当的大:

    由于访问的是辅助索引,如果查询需要访问聚集索引上的列,那么必要需要进行回表取数据,看似每条记录只是多了一次回表操作,但这才是index nested_loop join算法最大的弊端。

    • 首先,辅助索引的index lookup是比较随机I/O访问操作
    • 其次,根据index lookup再进行回表又是一个随机的I/O操作。所以说,index nested_loop join最大的弊端是其可能需要大量的离散操作,这在SSD出现之前是最大的瓶颈。而即使SSD的出现大幅提升了随机的访问性能,但是对比顺序I/O,其还是慢了很多,依然不在一个数量级上。

    3.3. block nested loop join

    block nested loop join 将外表中的需要join匹配的列(不是完整的记录)暂时保存在一块内存( Join Buffer )中,让后将这块内存中的数据内表进行匹配(内表只扫描一次)

    • block nested loop join 可被用于联接的是ALL,index,range的类型
    For each tuple r in R do
      store used columns as p from R in Join Buffer # 将部分或者全部R的记录保存到 Join Buffer中,记为p
          For each tuple s in S do
              If p and s satisfy the join condition # p 与 s满足join条件
                  Then output the tuple # 输出为结果集
                          block nested loop join 与simple nested loop join 相比,多了一个Join Buffer
    
    mysql> show variables like "%join%buffer%";
    +------------------+-----------+
    | Variable_name | Value |
    +------------------+-----------+
    | join_buffer_size | 134217728 | -- 128M,默认是256K
    +------------------+-----------+
    1 row in set (0.00 sec)
    

    Join Buffer 用的不是Buffer Pool中的内存,而是线程级别的内存。
    可以通过explain查看执行计划,并通过join条件字段的大小,来预估 join_buffer_size 的大小。
    注意:
    增大join_buffer_size 进行优化的前提是没有使用index ,如果使用了index,根本不会使用block nested join算法

    3.3.1 Join Buffer缓存的对象

    Join Buffer缓存的对象是所有参与查询的列都会保存到Join Buffer,而不是只有Join的列.

    比如下面的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可以预估需要分配的Join Buffer大小,然后尽量使得内表的扫描次数尽可能的少,最优的情况是只扫描内表一次。

    3.3.2 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。

    3.3.3 Block Nested-Loop Join总结

    Block Nested-Loop Join极大的避免了内表的扫描次数,如果Join Buffer可以缓存外表的数据,那么内表的扫描仅需一次,这和Hash Join非常类似。但是Block Nested-Loop Join依然没有解决的是Join比较的次数,其仍然通过Join判断式进行比较。综上所述,到目前为止各Join算法的成本比较如下所示:

    3.4. MariaDB中的Hash Join算法

    MySQL 目前不支持Hash Join

    由上图可知, MariaDB 中的Hash Join 算法是在Block Join 基础之上,根据Join Buffer中的对象创建哈希表,内表通过哈希算法进行查找,减少内外表扫描的次数,提升Join的性能

    • MariaDB中的Hash Join问题是,优化器默认不会去选择Hash Join算法

      • set join_cache_level = 4;
      • set optimizer_switch='join_cache_hashed=on';

    设置两个变量后,MariaDB将强制使用hash算法,无法智能判断

    3.5. batched key access join

    通过上图可以知道,在index join的基础上,增加MRR的功能,先对索引进行排序,然后批量获取聚集索引中的记录,由于使用了MRR的功能,所以默认该join算法也是不会被使用到的

    • set optimizer_switch='mrr_cost_based=off';
    -- 方法一
    mysql> SET optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on'; -- 在session中打开BKA特性
    mysql> explain SELECT * FROM part, lineitem WHERE l_partkey=p_partkey AND p_retailprice>3000G
    *************************** 1. row ***************************
    id: 1
    select_type: SIMPLE
    table: part
    partitions: NULL
    type: ALL
    possible_keys: PRIMARY
    key: NULL
    key_len: NULL
    ref: NULL
    rows: 195802
    filtered: 33.33
    Extra: Using where
    *************************** 2. row ***************************
    id: 1
    select_type: SIMPLE
    table: lineitem
    partitions: NULL
    type: ref
    possible_keys: i_l_suppkey_partkey,i_l_partkey
    key: i_l_partkey
    key_len: 5
    ref: dbt3.part.p_partkey
    rows: 27
    filtered: 100.00
    Extra: Using Join Buffer (Batched Key Access) -- 使用了BKA
    2 rows in set, 1 warning (0.00 sec)
    
    -- 方法二
    mysql> SET optimizer_switch='mrr=on,mrr_cost_based=on,batched_key_access=off'; -- 在session中关闭BKA特性
    mysql> explain SELECT /*+ BKA(lineitem)*/ * FROM part, lineitem WHERE l_partkey=p_partkey AND p_retailprice>2050G -- 使用/*+ BKA(tablename)*/ 的语法,强制使用BKA特性
    *************************** 1. row ***************************
    id: 1
    select_type: SIMPLE
    table: part
    partitions: NULL
    type: ALL
    possible_keys: PRIMARY
    key: NULL
    key_len: NULL
    ref: NULL
    rows: 195802
    filtered: 33.33
    Extra: Using where
    *************************** 2. row ***************************
    id: 1
    select_type: SIMPLE
    table: lineitem
    partitions: NULL
    type: ref
    possible_keys: i_l_suppkey_partkey,i_l_partkey
    key: i_l_partkey
    key_len: 5
    ref: dbt3.part.p_partkey
    rows: 27
    filtered: 100.00
    Extra: Using Join Buffer (Batched Key Access) -- 使用了BKA
    2 rows in set, 1 warning (0.00 sec)
    

    二. MRR补充

    (root@localhost) 10:35:54 [employees]> show variables like "%optimizer_switch%"G;
    *************************** 1. row ***************************
    Variable_name: optimizer_switch
            Value: index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on
    1 row in set (0.01 sec)
    
    ERROR:
    No query specified
    
    (root@localhost) 10:36:05 [employees]>  explain select * from employees where hire_date >= '1996-01-01'G;
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: employees
       partitions: NULL
             type: range
    possible_keys: idx_date
              key: idx_date
          key_len: 3
              ref: NULL
             rows: 41036
         filtered: 100.00
            Extra: Using index condition
    1 row in set, 1 warning (0.00 sec)
    
    (root@localhost) 10:36:06 [employees]>  explain select /*+ MRR(employees)*/ * from employees where hire_date >= '1996-01-01'G;
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: employees
       partitions: NULL
             type: range
    possible_keys: idx_date
              key: idx_date
          key_len: 3
              ref: NULL
             rows: 41036
         filtered: 100.00
            Extra: Using index condition; Using MRR    --使用MRR
    1 row in set, 1 warning (0.00 sec)
    
    ERROR:
    No query specified
    
    (root@localhost) 10:36:46 [employees]>
    
    

    如果强制开启MRR,那在某些SQL语句下,性能可能会变差;因为MRR需要排序,假如排序的时间超过直接执行的时间,那性能就会降低。
    optimizer_switch 可以是全局的,也可以是会话级的

    [参考MySQL Join算法与调优白皮书]

  • 相关阅读:
    《面向对象》读书笔记4
    《面向对象》读书笔记3
    《面向对象》读书笔记2
    《面向对象》读书笔记1
    B树
    树的子结构
    最长公共子序列
    最长公共子串
    堆和堆排序
    位图的原理和简单实现
  • 原文地址:https://www.cnblogs.com/gczheng/p/8268874.html
Copyright © 2011-2022 走看看