美文网首页
mysql 联接查询算法之实践篇(六)和优化join总结

mysql 联接查询算法之实践篇(六)和优化join总结

作者: 尹楷楷 | 来源:发表于2020-12-15 09:01 被阅读0次

    mysql选择使用连接算法的优先级
    在选择Join算法时,会有优先级,理论上会优先判断能否使用INLJ、BNLJ:
    内部表关联字段上有索引 Index Nested-LoopJoin > 5.7 Block Nested-Loop Join、8.0 Hash Join > Simple Nested-Loop Join
    示例
    1、数据准备

    表结构

    CREATE TABLE `user`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
    CREATE TABLE `book`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `user_id` int(11) NULL DEFAULT NULL,
      `book_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE,
      INDEX `index_user_id`(`user_id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    

    编写存储过程创建数据

    
    -- 随机字符串函数
    CREATE DEFINER=`root`@`localhost` FUNCTION `rand_string`(n int) RETURNS varchar(255) CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci
    begin
       declare chars_str varchar(100) default 'qwertyuiopasdfghjklzxcvbnm';
       declare return_str varchar(255) default '';
       declare i int default 0;
       while i<n do
       set return_str=concat(return_str,substring(chars_str,floor(1+rand()*52),1));
       set i=i+1;
       end while;
       return return_str;
     end
    
    -- addUser 添加用户信息存储过程
    CREATE DEFINER=`root`@`%` PROCEDURE `addUser`(in max_num int(10))
    begin
     declare i int default 0;
     repeat
     set i=i+1;
     INSERT INTO `test`.`user`(`name`) VALUES (rand_string(16));
     until i=max_num end repeat;
    
    end
    
    -- addUser 添加书籍信息存储过程
    CREATE DEFINER=`root`@`%` PROCEDURE `addBook`(in max_num int(10))
    begin
     declare i int default 0;
     repeat
     set i=i+1;
    INSERT INTO `test`.`book`(`user_id`, `book_name`) VALUES (FLOOR( 1 + RAND() * (1000 - 1)), rand_string(20));
    
     until i=max_num end repeat;
    
    end
    

    2、给 user表添加1000条,book表添加10万条。1:100

    3、看看执行计划

    EXPLAIN SELECT * FROM `user` a LEFT JOIN book  b IGNORE index(index_user_id)  ON a.id=b.user_id
    

    A、强制不走内表的索引的join,使用了BNL。查询时间7.298秒。


    image.png

    B、相反,强制走索引。使用了INLJ,查询时间0.210秒!

    EXPLAIN SELECT a.name,b.book_name FROM `user` a LEFT JOIN book  b force index(index_user_id)  ON a.id=b.user_id
    
    image.png

    不能在Extra字段中看到是否使用INLJ算法,但是可以通过key观察有没有用到索引

    C、再来看看,将BNLJ关闭。查询会变得多慢。。先看执行计划

    SET optimizer_switch = 'block_nested_loop=off'; 
    EXPLAIN SELECT *  FROM `user` a LEFT JOIN book  b force index(index_user_id)  ON a.id=b.user_id
    
    image.png

    果然,连BNLJ都不使用的话会变成30秒!这时候应该使用的是SNLJ连接算法。


    image.png

    使用SNLJ也不会体现在Extra,但是我们观察到key字段是空的。

    D、开启BKA算法,看看情况

    SET optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';
    EXPLAIN SELECT *  FROM `user` a LEFT JOIN book  b force index(index_user_id)  ON a.id=b.user_id
    
    image.png

    耗时 0.276秒。从我这里的实验结果来看BKA相对于INLJ来说并没有很明显的优化,可能是我的实验姿势不对吧。

    总结下

    BKA > INLJ (可能成立)0.22s
    > BNLJ (成立) 8s
    > SNLJ (成立) 30s

    mysql5.7对于join查询总是默认先使用 INLJ,内部表关联字段没加索引得话就去使用BNLJ 。若block_nested_loop=off 那么就回去使用SNLJ了。

    而BKA默认是关闭的,这方面mysql比较保守。想要在二者BKA和INLJ之间做出抉择可以自行测试对比二者性能。

    再看看mysql8中的hash join 性能测试

    image.png
    EXPLAIN  SELECT *  FROM `user` a LEFT JOIN book  b IGNORE index(index_user_id)  ON a.id=b.user_id
    
    image.png

    注意,这里为什么要IGNORE 忽略使用索引呢? mysql优先会使用INLJ而不是 hash join。而hash join 是不需要索引支持的,hashjoin在mysql8中是一种代替 BNLJ的一种更高效的join算法。

    查询时间0.497秒,相对于上面mysql 5.7中使用BNLJ 的8秒可是快了很多。

    最后总结下如何优化Join速度

    由于连接的成本比较高,因此对于高并发的应用,应该尽量减少有连接的查询,连接的表的个数不能太多,连接的表建议 控制在4个以内。互联网应用比较常见的一种情况是,在数据量比较小的时候,连接的开销不大,这个时候一般不会有性能问 题,但当数据量变大之后,连接的低效率问题就暴露出来了,成为整个系统的瓶颈所在。所以对于数据库应用的设计,最好在 早期就确定未来可能会影响性能的一些查询,进行反范式设计减少连接的表,或者考虑在应用层进行连接。 优化连接的一些要点如下。

    1、使用EXPLAIN检查连接,留意EXPLAIN输出的rows列,如果rows列太高,比如几千,上万,那么就需要考虑是否索引不佳或连接表的顺序不当。用小结果集驱动大结果集,减少外层循环的数据量,从而减少内层循环次数:如果小结果集和大结果集连接的列都是索引列,mysql在内连接时也会选择用小结果集驱动大结果集,因为索引查询的成本是比较固定的,这时候外层的循环越少,join的速度便越快。

    2、为内部表的关联字段上增加索引:争取使用INLJ,减少内层表的循环次数;ON、USING子句中的列确认有索引。如果优化器选择了连接的顺序为B、A,那么我们只需要在A表的列上创建索引即可。例如,对于查询“SELECT B.,A.FROM B JOIN A ON B.col1=A.col2;”语句MySQL会全表扫描B表,对B表的每一行记录探测 A表的记录(利用A表col2列上的索引)。

    3、增大join buffer size的大小:当使用BNLJ/Hash Join时,一次缓存的数据越多,那么内层表循环的次数就越少

    4、减少不必要的字段查询:
    (1)当用到BNLJ时,字段越少,join buffer 所缓存的数据就越多,内层表的循环次数就越少;
    (2)当用到INLJ时,如果可以不回表查询,即利用到覆盖索引,则可能可以提升速度。(未经验证,只是一个推论)

    5、最好是能转化为INNER JOIN,LEFT JOIN的成本比INNERJOIN高很多。

    6、反范式设计,增加冗余字段。这样可以减少连接表的个数,加快存取数据的速度。

    7、考虑在应用层实现连接。 对于一些复杂的连接查询,更值得推荐的做法是将它分解为几个简单的查询,可以先执行查询以获得一个较小的结果集, 然后再遍历此结果集,最后根据一定的条件去获取完整的数据,这样做往往是更高效的,因为我们把数据分离了,更不容易发 生变化,更方便缓存数据,数据也可以按照设计的需要从缓存或数据库中进行获取。例如,对于如下的查询: SELECT a.* FROM a WHERE a.id IN (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17); 如果id=1~15的记录已经被存储在缓存(如Memcached)中了,那么我们只需要到数据库查询“SELECT a.FROMa WHERE a.id=16”和“SELECT a.FROMa WHERE a.id=17”了。而且,把IN列表分解为等值查找,往往可以提高性能。

    8、一些应用可能需要访问不同的数据库实例,这种情况下,在应用层实现连接将是更好的选择。

    相关文章

      网友评论

          本文标题:mysql 联接查询算法之实践篇(六)和优化join总结

          本文链接:https://www.haomeiwen.com/subject/csdaiktx.html