数据库监控与调优【十二】—— JOIN语句优化

news2024/12/22 6:17:26

JOIN语句优化-JOIN种类、算法与原理

JOIN的种类

在这里插入图片描述

笛卡尔连接(cross join)

-- 举例:通过笛卡尔连接查询两张表的结果集和单查两张表的结果集对比
SELECT count( * ) FROM users a CROSS JOIN orders b;
SELECT ( SELECT count( * ) FROM users ) * ( SELECT count( * ) FROM orders );

-- 如果 cross join 带有on子句,就相当于inner join
SELECT * FROM users a CROSS JOIN orders b ON a.id = b.user_id;
SELECT * FROM users a INNER JOIN orders b ON a.id = b.user_id;

JOIN算法1-Nested-Loop Join(NLJ)

翻译成中文叫嵌套循环Join

在这里插入图片描述

如图,存在三张表,t1、t2、t3,使用NLJ算法,执行过程大概是这样的,先查询t1中符合条件的数据,然后通过for循环遍历这些数据,在循环中查询t2表符合条件的数据,并使用reference key匹配,也就是join中的on条件。接着,再查询出第三张表中数据匹配,由于第三张表是全表扫描,所以第三张表直接是一个for循环,循环t3表全部数据,判断满足条件就返回给客户端。

不难发现,NLJ算法是比较简单粗暴的,外层循环结果集越多,内层循环扫描的次数就越多。如果外层循环的表数据量大,比如t1表数据量大,执行效率是非常地下的。

JOIN算法2-Block Nested-Loop Join(BNLJ MySQL 5.6引入)

翻译成中文叫块嵌套循环join

在这里插入图片描述

如图,还是刚刚三张表,查询t1和t2和之前一样,但是查询t3表就存在区别,把t1和t2表需要用到的字段存储到join buffer(连接缓存)。一直存入join buffer数据,当join buffer满时,开始循环t3表,用t3表的数据和join buffer里面的数据比较,如果匹配就返回给客户端。

对比NLJ和BNLJ

如果使用NLJ,for each row in t2如果存在100个元素就得执行100次for each row in t3,扫描t3表100次,总共需要扫描t3表10000次。

如果使用BNLJ,mysql会把这100条数据缓存到join buffer,如果join buffer足够大,会把这100条数据都存放到缓存,只要执行一次for each row in t3即可,这样,大幅度减少了内存表扫描次数。

但是如果这100条数据无法全部放到join buffer,需要扫描多少次t3表存在以下计算公式。

  • (S * C)/join_buffer_size + 1
    • S:缓存的t1/t2表的一行数据大小
    • C:缓存的行数
    • join_buffer_size:join buffer的大小

假设,第一层有100条数据,第二层有100条数据,join buffer中可以存放t1/t2数据为100条,则需要100 * 100 / 100 + 1=101,需要扫描101次。

使用join buffer的条件

  • 只有在连接类型为ALL、index或range的时候才可以使用join buffer

  • 第一个nonconst table不会分配join buffer,即使连接类型是ALL或index

  • join buffer只会缓存需要的字段,而非整行数据

  • 可以通过join_buffer_size变量设置join buffer大小

    • -- 查看默认的join_buffer_size,默认为262144字节,即256K
      -- 一般建议设置比较小的全局join_buffer_size,默认值比较合适
      SHOW VARIABLES LIKE 'join_buffer_size';
      
      -- 设置当前会话的join_buffer_size
      SET join_buffer_size = 1024 * 1024 * 50;
      
      -- 设置全局的join_buffer_size
      SET GLOBAL join_buffer_size = 1024 * 1024 * 50;
      
  • 每个能被缓存的join都会分配一个join buffer,一个查询可能拥有多个join buffer

  • join buffer在执行连接之前会分配,在查询完成后会释放

如何知道一条sql使用了BNLJ?

-- Extra中出现:Using join buffer (Block Nested Loop)表示使用了BNLJ
EXPLAIN SELECT * FROM users a LEFT JOIN orders b ON a.id = b.user_id;

JOIN算法3-Batched Key Access Join(BKA MySQL 5.6引入)

  • BKA的基石:Multi Range Read(MRR)

    • -- salaries表存在from_date和to_date的组合索引
      -- 该查询会使用到索引,但是可能会伴随大量的随机IO
      -- 因为数据是按照主键排列,而不是按照from_date字段排列
      -- 使用MRR可以优化这个随机IO,MRR不是扫描索引,检查到索引里面某一行再去表数据获取数据。而是把符合条件的索引都丢到缓存里面
      /*
      	比如扫描到索引:
      	[from_date,to_date,(id,from_date)]
      	[1979-06-06,1980-06-06,(30000,1979-06-06)]
      	[1978-06-06,1979-06-06,(20000,1978-06-06)]
      	[1968-06-06,1969-06-06,(80000,1968-06-06)]
      	之后会按照主键排序:
      	[1978-06-06,1979-06-06,(20000,1978-06-06)]
      	[1979-06-06,1980-06-06,(30000,1979-06-06)]
      	[1968-06-06,1969-06-06,(80000,1968-06-06)]
      	排序完成之后再到表中读取数据
      	而B+Tree数据结构里面的叶子节点都是按照主键排序的
      	而现在MRR也是按照主键顺序排序,在读取数据时就会比较接近顺序IO
      	顺序IO的性能要比随机IO的性能好很多,查询就会快很多
      */
      EXPLAIN SELECT * FROM salaries WHERE from_date <= '1980-01-01'; 
      
  • MRR核心:将随机IO转换为顺序IO,从而提升性能

    • 当然,使用MRR也不一定有性能提升,因为带来了排序的开销

MRR参数

  • optimizer_switch的子参数

    • mrr:是否开启mrr,on开启,off关闭

    • mrr_cost_based:表示是否要开启基于成本计算的MRR,on开启,off关闭

    • -- 查询mrr默认值
      -- 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,use_invisible_indexes=off,skip_scan=on,hash_join=on,subquery_to_derived=off,prefer_ordering_index=on,hypergraph_optimizer=off,derived_condition_pushdown=on
      -- 可以看到mrr=on,mrr_cost_based=on,表示mrr开启,基于成本计算的mrr也开启
      SHOW VARIABLES LIKE '%optimizer_switch%';
      
  • read_rnd_buffer_size:指定mrr缓存大小,即存放排序后索引内存大小

    • -- 查询mrr默认缓存大小
      -- 默认是262144字节,即256K
      SHOW VARIABLES LIKE '%read_rnd_buffer_size%';
      

由于mrr_cost_based的计算非常保守,所以默认情况下EXPLAIN SELECT * FROM salaries WHERE from_date <= '1980-01-01'; 这个例子是不会使用mrr的。可以通过Extra参数项查看。里面是Using index condition

可以通过关闭mrr_cost_based,实现以上例子使用mrr

SET optimizer_switch = 'mrr_cost_based=off';

再次执行上面的例子,可以看到Extra参数项里面显示Using index condition; Using MRR,表示使用了mrr

BKA流程

之前NLJ和BNLJ,如果每匹配到一条数据,就去读取一次表数据,IO操作太频繁,而且往往都是随机IO,性能很差。

在这里插入图片描述

假设t3表是存在索引的,从join buffer中读取数据,用t3表的索引和join buffer匹配,筛选出匹配的索引之后,再通过MRR对t3表的主键进行排序,最后再读取t3表的数据。

BKA的参数

  • optimizer_switch的子参数

    • batched_key_access:on开启,off关闭

    • -- 查询batched_key_access默认值
      -- 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,use_invisible_indexes=off,skip_scan=on,hash_join=on,subquery_to_derived=off,prefer_ordering_index=on,hypergraph_optimizer=off,derived_condition_pushdown=on
      -- 可以看到batched_key_access=off,表示BKA算法默认是关闭的
      SHOW VARIABLES LIKE '%optimizer_switch%';
      
    • -- 开启BKA
      SET optimizer_switch = 'batched_key_access=on';
      
    • -- 测试使用BKA查询的情况
      -- 当使用BKA的时候,会在Extra里面展示Using join buffer (Batched Key Access)
      EXPLAIN SELECT * FROM salaries a, employees b WHERE a.from_date = b.birth_date;
      

JOIN算法4-HASH JOIN(MySQL 8.0.18引入 替代BNLJ)

  • MySQL 8.0.18引入,用来替代BNLJ
  • 选择一个较小的表,以减少建立哈希表的时间和空间,对其中每个元素上的连接属性(join attribute)采用哈希函数得到哈希值,从而建立一个哈希表。对另一个表,扫描它的每一行并计算连接属性的哈希值,与bulid phase建立的哈希表对比,若有落在同一个bucket的,如果满足连接谓词(predicate)则连接成新的表。在内存足够大的情况下建立哈希表的过程时整个表都在内存中。本质上就是join buffer缓存外部循环的hash表,内层循环遍历时到hash表匹配
  • 原理剖析好文:https://mysqlserverteam.com/hash-join-in-mysql-8/

HASH JOIN注意点

  • MySQL 8.0.18才引入,且有很多限制,比如不能作用于外连接,比如left join/right join等。从MySQL 8.0.20开始,限制少了很多,外连接可以作用,建议使用MySQL 8.0.20或更高版本
  • 从MySQL 8.0.18开始,hash join的join buffer时递增分配的,这意味着,你可以将join_buffer_size设置的比较大。具体是这样的,一开始可能join_buffer_size只是256K,后续因为不够用会进行自动扩容,一直扩容到设置的值为止。而在MySQL 8.0.18中,如果使用了外连接,外连接没法用hash join,此时join_buffer_size会按照设置的值直接分配内存,因此join_buffer_size还是得谨慎设置
  • 从MySQL 8.0.20开始,BNLJ已被删除了,用hash join替代了BNLJ
  • HASH JOIN注意点好文推荐:https://dev.mysql.com/doc/refman/8.0/en/hash-joins.html

另外之前测试的sql语句EXPLAIN SELECT * FROM users a LEFT JOIN orders b ON a.id = b.user_id;是基于MySQL 8.0.18的。在不同版本的mysql中使用的算法是不同的

-- Extra中出现:Using join buffer (Block Nested Loop)表示使用了BNLJ   	基于MySQL 8.0.18
-- Extra中出现:Using where; Using join buffer (hash join)表示使用了hash join     基于MySQL 8.0.20
EXPLAIN SELECT * FROM users a LEFT JOIN orders b ON a.id = b.user_id;

JOIN语句优化-如何优化JOIN语句

驱动表 vs 被驱动表

  • 外层循环的表是驱动表,内层循环的表是被驱动表

在这里插入图片描述

还是之前的三张表,因此,t1是t2的驱动表,t2是t1的被驱动表。t2是t3的驱动表,t3是t2的被驱动表

例如:

EXPLAIN SELECT
	* 
FROM
	employees e
	LEFT JOIN dept_emp de ON e.emp_no = de.emp_no
	LEFT JOIN departments d ON de.dept_no = d.dept_no 
WHERE
	e.emp_no = 10001;

在这里插入图片描述

通过执行结果,可以根据id列显示的规则(如果explain的结果包括多个id值,则数字越大越先执行;而对于相同id的行,则表示从上往下依次执行),得出执行顺序是e->de->d,所以e表就是de表的驱动表,de表又是d表的驱动表

JOIN调优原则

  • 用小表驱动大表,即用数据量比较小的表作为驱动表,数据量比较大的表作为被驱动表

    • 一般不需要人工考虑,关联查询优化器会自动选择最优的执行顺序

      -- 尽管s表写在前面,但是mysql依然使用e表作为驱动表
      -- 其中s表2,838,426行数据,e表292,025行数据,mysql评估e表数据更少,故将e表作为驱动表
      EXPLAIN SELECT
      	* 
      FROM
      	salaries s
      	LEFT JOIN employees e ON s.emp_no = e.emp_no 
      WHERE
      	e.emp_no = 10001;
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mQwOeeFm-1687789650751)(.\picture\SQL\QQ图片20230523151303.png)]

    • 如果优化器抽风,可使用STRAIGHT_JOIN优化,强制使用左表作为驱动表

      -- 此时mysql会使用s表作为驱动表
      EXPLAIN SELECT
      	* 
      FROM
      	salaries s
      	STRAIGHT_JOIN employees e ON s.emp_no = e.emp_no 
      WHERE
      	e.emp_no = 10001;
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jqv9NHrO-1687789650751)(.\picture\SQL\QQ图片20230523151945.png)]

  • 如果有where条件,应当要能够使用索引,并尽可能的减少外层循环的数据量

    • 不管是使用NLJ、BNLJ、BKA、HASH JOIN算法,外层循环的数据量越大,内层扫描表的次数也就越多
  • join的字段尽量创建索引

    • 需要注意的是,联表查询存在隐式转换可能会导致索引失效的问题

      -- 如果s表的字段emp_no和e表的字段emp_no类型不一致,就可能导致索引失效的问题
      EXPLAIN SELECT
      	* 
      FROM
      	salaries s
      	LEFT JOIN employees e ON s.emp_no = e.emp_no 
      WHERE
      	e.emp_no = 10001;
      
    • 故join字段的类型要保持一致

  • 尽量减少扫描的行数(explain-rows)

    • 尽量控制在百万以内(经验之谈,仅供参考)
  • 参与join的表不要太多

    • 阿里编程规约建议不超过3张

    • 对于NLJ,表越多,循环嵌套越多,扫描表的次数也就越多

    • 而HASH JOIN对于多张表join的支持,理论上会好些,但是要在MySQL 8.0.18才会支持,业界使用这个版本的MySQL公司较少

    • 如果业务就是要操作多张表,可以根据业务把一条操作多张表的sql分为多条sql,再通过代码将查询结果组装成最后想要的数据

      -- 拆分前
      SELECT
      	* 
      FROM
      	employees e
      	LEFT JOIN dept_emp de ON e.emp_no = de.emp_no
      	LEFT JOIN departments d ON de.dept_no = d.dept_no 
      WHERE
      	e.emp_no = 10001;
      	
      
      -- 拆分后
      SELECT * FROM employees WHERE emp_no = 10001;
      SELECT * FROM dept_emp WHERE emp_no = 10001;
      SELECT * FROM departments WHERE dept_no = 'd005';
      

      拆分后的sql尽管要写一堆业务代码,而且要和数据库通信三次,但是拆分后执行都是简单的sql,并且都是基于主键的,性能非常好。在一些情况下,拆分之后的sql可能比拆分之前的sql性能更好

      再者,拆分之后的sql很简单,所以优化起来比较容易,复杂sql操作了多张表,执行计划很复杂,分析和优化都很困难

      所以不要以编写复杂sql为荣!

  • 如果被驱动表的join字段用不了索引,且内存较为充足,可以考虑把join buffer设置得大一些

    • join buffer设置大一些,可以减少内部循环的次数

ps:实际项目中,如果遇到比较诡异的问题,可以在mysql终端使用show warnings;语句查看问题。执行完EXPLAIN语句后跟show warnings;语句即可

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/692984.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot + Vue前后端分离项目实战 || 四:用户管理功能实现

系列文章&#xff1a; SpringBoot Vue前后端分离项目实战 || 一&#xff1a;Vue前端设计 SpringBoot Vue前后端分离项目实战 || 二&#xff1a;Spring Boot后端与数据库连接 SpringBoot Vue前后端分离项目实战 || 三&#xff1a;Spring Boot后端与Vue前端连接 文章目录 前端…

微服务: sleuth和zipkin的用处与zipkin安装使用(下)

目录 0. 上篇传送门: 1. 前言简介 mq安装传送门: 微服务: 01-rabbitmq的应用场景及安装(docker) 1.1 Sleuth是一款分布式跟踪解决方案。 1.2 Zipkin是一个开源的分布式跟踪系统。 2. zipkin安装方式 2.1 windows下安装zipkin: 2.1.0 下载jar包位置 2.1.1 下载后,找…

数值计算例题整理

数值计算 一、误差的来源和分类二、有效数字第一个大题&#xff08;非线性方程组的迭代法&#xff09;第二个大题&#xff08;LU分解&#xff09;第三个大题&#xff08;牛顿插值法&#xff09;第四个大题&#xff08;直线拟合&#xff09; 一、误差的来源和分类 误差是描述数…

Git 原理和使用

Git 安装 Git是开放源代码的代码托管⼯具&#xff0c;最早是在Linux下开发的。开始也只能应⽤于Linux平台&#xff0c;后⾯慢慢的被移植到windows下&#xff0c;现在&#xff0c;Git可以在Linux、Unix、Mac和Windows这⼏⼤平台上正常运⾏了。 Linux-centos 安装git sudo yu…

8.3 PowerBI系列之DAX函数专题-矩阵Matrix中高亮显示最大最小值

需求 用颜色标量年度最大最小值 用颜色标示折线的最大值最小值 实现 在条件格式–规则–基于字段进行计算 度量值 is_max_min var displayed_data calculatetable( addcolumns( summarize(‘订单表’&#xff0c;‘产品表’[商品次级类别]&#xff0c;‘订单表’[订单日…

arcgis栅格影像裁剪--shp

1、打开软件&#xff0c;导入数据&#xff0c;如下&#xff1a; 2、裁剪面形状如下&#xff0c;为shp文件&#xff1a; 3、在arctoolbox中找到"数据管理工具"--"栅格"--"栅格处理"--"裁剪"工具&#xff0c;如下&#xff1a; 4、打开裁…

(ESP32)报错-portTICK_RATE_MS‘ undeclared

&#xff08;ESP32&#xff09;报错-portTICK_RATE_MS undeclared 问题详情ESP- IDF未正确设置 问题详情 报错提示 portTICK_RATE_MS undeclared (first use in this function); did you mean portTICK_PERIOD_MS?具体情况 已经引用相关头文件&#xff0c;并且右键后可以大概…

leetcode 2462. Total Cost to Hire K Workers(雇用 K 名员工的总成本)

每次从 开头candidates个 和 末尾candidates个 工人中选择一个cost最小的。 如果有2个工人cost相同&#xff0c;就选index较小的。 每个工人的cost在数组costs里。 直到雇够k个工人。 问雇k个工人需要多少cost. 思路&#xff1a; 可以考虑用一个优先队列&#xff0c;按cost排…

2023开放原子全球开源峰会——一场开发者的盛宴

文章目录 上午场下午场开发者之夜 #“2023我在开源峰会”特别征文# 2023开放原子全球开源峰会&#xff0c;6月11日-13日在北京盛大召开&#xff0c;开幕第一天正好是周六&#xff0c;没什么事情&#xff0c;一大早就过去了&#xff0c;早晨大概7点出发&#xff0c;公交、地铁一…

Docker Desktop 安装使用教程

一、前言 作为开发人员&#xff0c;在日常开发中&#xff0c;我们需要在本地去启动一些服务&#xff0c;如&#xff1a;redis、MySQL等&#xff0c;就需要去下载这些在本地去启动&#xff0c;操作较为繁琐。此时&#xff0c;我们可以使用Docker Desktop&#xff0c;来搭建我们需…

php+mysql期末作业小项目

目录 1、登录界面 2、注册界面 3、主界面 4、学生表界面 5 、查询学生界面​编辑 6、修改学生信息界面​编辑 7、删除学生信息界面 8、添加学生信息界面 9、后台数据库​编辑 一个简单的php➕mysql项目学生信息管理系统&#xff0c;用于广大学子完成期末作业的参考&…

Android Studio导入flutter项目,运行和调试按钮灰色

描述&#xff1a;用android Studio导入flutter项目&#xff0c;运行和调试按钮无法点击并置灰&#xff0c;显示如下 解决方法&#xff1a;检查是否设置如下内容&#xff1a; 1.是否配置了Android SDK &#xff0c;打开 file > project Structure >project 2.是否配置了F…

【架构】领域驱动设计(DDD)的几种典型架构介绍

文章目录 前言一、专业术语二、架构演变三、限界上下文四、领域驱动设计的四重边界五、整洁分层架构六、六边形架构七、洋葱架构总结 前言 我们生活中都听说了DDD&#xff0c;也了解了DDD&#xff0c;那么怎么将一个新项目从头开始按照DDD的过程进行划分与架构设计呢&#xff…

【Web自动化测试】如何生成高质量的测试报告

运行了所有测试用例&#xff0c;控制台输入的结果&#xff0c;如果很多测试用例那也不能够清晰快速的知道多少用例通过率以及错误情况。 web自动化测试实战之批量执行测试用例场景: 运行 AllTest.py 文件后得到的测试结果不够专业&#xff0c;无法直观的分析测试结果,我们能否…

如何学习和提高CAPL语言编程能力

CAPL是Vector公司开发的&#xff0c;用来配合它的系列产品使用的一款面向过程的语言。CAPL是Communication Access Programming Language的缩写&#xff0c;从字面意思来说&#xff0c;是专门用于通信访问的编程语言。 最初访问CAN总线&#xff0c;现在已扩展到所有的汽车总线…

代理模式(Proxy)

定义 代理是一种结构型设计模式&#xff0c;让你能够提供对象的替代品或其占位符。代理控制着对于原对象的访问&#xff0c;并允许在将请求提交给对象前后进行一些处理。 前言 1. 问题 举个例子&#xff1a;有这样一个消耗大量系统资源的巨型对象&#xff0c; 你只是偶尔需…

mac部署fastadmin踩坑记录

粘贴一下解决配置&#xff0c;主要Nginx配置问题 //后台NGINX location / {if (!-e $request_filename) {rewrite ^(.?\.php)(/.)$ /$1?s$2 last;# 加上这一句配置试试rewrite ^(.*)$ /ewgadmin.php?s$1 last; # 对应项目修改对应入口文件break;}}//接口文档Nginx配置 loca…

sql server还原新数据库,解决原库还原中...

1&#xff09;用studyDB库&#xff0c;备份出数据库备份文件 studyDB_backup_2023_06_19.bak 2&#xff09;用备份文件 studyDB_backup_2023_06_19.bak还原数据新库CollegeStudyDB和原库studyDB到同一服务器 3&#xff09;数据库CollegeStudyDB按原成功&#xff0c;但是原库s…

Linux环境准备以及CentOS7.6系统安装

&#xff08;该图由AI绘制 本人提供教学 FREE&#xff09; 运维概述与Linux系统安装 一、VMware虚拟机 1、什么是虚拟机 其实虚拟机就是在Windows的真机上创建一个独立的其他操作系统的运行环境而且其对宿主机&#xff08;Windows&#xff09;没有任何影响。 2、虚拟机的种…

《操作系统》by李治军 | 实验7 - 地址映射与共享

目录 一、实验目的 二、实验内容 &#xff08;一&#xff09;跟踪地址翻译过程 &#xff08;二&#xff09;基于共享内存的生产者—消费者程序 &#xff08;三&#xff09;共享内存的实现 三、实验准备 1. Linux 中的共享内存 2. 获得空闲物理页面 3. 地址映射 4. 寻…