一文详解MySQL—Join的使用优化

目录
  • MySQL JOIN类型
  • MySQL JOIN 算法
    • Nested-Loop Join 算法
      • 执行流程
      • 工作原理
      • 时间复杂度分析
    • Block Nested-Loop Join 算法
      • 执行流程
      • 工作原理
      • 时间复杂度分析
    • Hash Join 算法
      • 执行流程
      • build 构建
      • probe 探测阶段
      • 如何使用
    • 时间复杂度分析
    • NLJ算法优化
    • BNL算法优化
    • Hash Join算法优化

MySQL JOIN类型

MySQL支持多种JOIN类型,下面是每种JOIN类型的简要概述:

  • INNER JOIN:将两个表中符合条件的行组合在一起。返回的结果集只包含满足连接条件的行,即两个表中都存在的行。一般简写成JOIN
  • LEFT JOIN:返回左表中的所有行以及符合条件的右表中的行。如果右表中没有匹配的行,则用NULL填充。
  • RIGHT JOIN:返回右表中的所有行以及符合条件的左表中的行。如果左表中没有匹配的行,则用NULL填充
  • FULL OUTER JOIN:返回左表和右表中的所有行,如果一个表中没有匹配的行,则用NULL填充。
  • CROSS JOIN:返回两个表中的所有可能的组合,也称为笛卡尔积。

MySQL JOIN 算法

在了解完 MySQL JOIN类型概念之后,我们来了解 MySQL JOIN 算法,在之前的版本只支持Nested Loop Join这一种算法,在 MySQL 8.0.18之后支持 Hash Join算法。

Nested-Loop Join 算法

执行流程

假设两个表一个 user 用户表,一个 order 订单表,需要查询用户的所有订单信息,表结构如下:

CREATE TABLE `user` (
  `id` int(11) NOT NULL COMMENT '主键id',
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户名称',
  `age` int(255) DEFAULT NULL COMMENT '年龄',
  `user_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户编号',
  PRIMARY KEY (`id`),
  KEY `idx_user_code` (`user_code`) USING BTREE COMMENT '用户编号索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';

CREATE TABLE `order` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `order_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '订单名称',
  `user_code` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户编号',
  PRIMARY KEY (`id`),
  KEY `idx_user_code` (`user_code`) USING BTREE COMMENT '用户编号索引'
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='订单表';

其中 user_code是连接字段,也是索引。SQL 如下:

mysql> SELECT
	u.`name`,
	u.user_code,
	o.order_name
FROM
	`user` u
	JOIN `order` o ON u.user_code = o.user_code;
+------+-----------+------------+
| name | user_code | order_name |
+------+-----------+------------+
| 李   | 002       | 订单1      |
| 李   | 002       | 订单2      |
| 李   | 002       | 订单3      |
| 李   | 002       | 订单4      |
| 李   | 002       | 订单5      |
| 李   | 002       | 订单6      |
| 李   | 002       | 订单7      |
| 李   | 002       | 订单8      |
| 李   | 002       | 订单9      |
+------+-----------+------------+
9 rows in set (0.08 sec)

看一下这条语句的explain结果

这个语句的执行流程如下:

  • 从表order中读入一行数据 ;
  • 从数据行中, 取出user_code字段到表user里去查找;
  • user表根据索引找到满足条件的行字段, 跟上面的数据行组成一行;
  • 重复执行步骤1到3, 直到表user的末尾循环结束。

工作原理

这个过程就跟我们写程序时的嵌套查询,一般称为Nested-Loop Join (NLJ),是一种最基本的Join算法,它通过对两个表进行嵌套循环来查找匹配的行。具体来说,它从一个表中取出一行,然后在另一个表中扫描所有行,查找与之匹配的行。类似于这样:

for each row in t1 matching range {
  for each row in t2 matching reference key {
    for each row in t3 {
      if row satisfies join conditions, send to client
    }
  }
}

时间复杂度分析

这个过程将会对每一行进行一次比较,因此它的时间复杂度为:O(m∗n)O(m*n)O(m∗n),其中 mn 分别是两个表的行数。

假设被驱动表的行数是M。 每次在被驱动表查一行数据, 要先搜索索引a, 再搜索主键索引。每次搜索一棵树近似复杂度是logMlog MlogM, 所以在被驱动表上查一行的时间复杂度是 2∗logM2*log M2∗logM。

假设驱动表的行数是N, 执行过程就要扫描驱动表N行, 然后对于每一行, 到被驱动表上匹配一次。因此整个执行过程, 近似复杂度是 N+N∗2∗logMN + N*2*log MN+N∗2∗logM。 N对扫描行数的影响更大, 因此应该让小表来做驱动表。对于大型数据集来说,它的性能会变得非常慢,因为它需要对一个表的每一行都扫描另一个表。

Block Nested-Loop Join 算法

执行流程

接下来, 我们删除掉索引,再看看被驱动表用不上索引的情况

ALTER TABLE `user` DROP INDEX `idx_user_code`;
EXPLAIN SELECT
	u.`name`,
	u.user_code,
	o.order_name
FROM
	`user` u
	JOIN `order` o ON u.user_code = o.user_code

再看一下这条语句的explain结果,多了一个 Using join buffer (Block Nested Loop

这个时候语句的执行流程如下:

  • 从表user中读入name,user_code字段数据放到线程内存join_buffer
  • 扫描表order表, 把表order中的每一行取出来, 跟join_buffer中的数据做对比, 满足join条件的, 作为结果集的一部分返回。

工作原理

Join Buffer是一种内存缓存,并在查询完成后释放,它可以在执行时缓存Join的中间结果。join_buffer 的大小是由参数join_buffer_size设定的, 默认值是256k。

mysql> show variables like '%join_buffer_size%';
+------------------+---------+
| Variable_name    | Value   |
+------------------+---------+
| join_buffer_size | 1048576 |
+------------------+---------+
1 row in set (0.10 sec)

那如果Join Buffer中放不下表user的所有数据话会怎么处理呢? 策略很简单, 就是分段 ,每次清空join_buffer

for each row in t1 matching range {
    store used columns from t1 in join buffer
    ## 如果buffer满了
	if buffer is full {
      for each row in t2 {
        for each t1 combination in join buffer {
		  ## 如果行满足连接条件,则发送到客户端
          if row satisfies join conditions, send to client
        }
      }
	  ## 清空buffer
      empty join buffer
    }

}

if buffer is not empty {
  for each row in t2 {
    for each t1 combination in join buffer {
      if row satisfies join conditions, send to client
    }
  }
}

时间复杂度分析

可以看到,在这个过程中, 对表userorder都做了一次全表扫描。 因此它的时间复杂度为:O(M+N)O(M+N)O(M+N)。由于join_buffer是以无序数组的方式组织的, 因此对表order中的每一行, 都要做判断。假设小表的行数是N, 大表的行数是M, 那么在这个算法里:

  • 两个表都做一次全表扫描, 所以总的扫描行数是M+NM+NM+N;
  • 内存中的判断次数是M∗NM*NM∗N

Block Nested-Loop Join(BNL)是一种优化的NLJ算法,BNL 通过将一个表分成多个块(block),然后逐个块与另一个表进行Join操作,从而减少了不必要的重复扫描和比较。它可以提高NLJ在处理大数据集时的性能,但是会占用过多的CPU资源。会多次扫描被驱动表,占用磁盘IO资源。

Hash Join 算法

Beginning with MySQL 8.0.20, support for block nested loop is removed, and the server employs a hash join wherever a block nested loop would have been used previously.(dev.mysql.com/doc/refman/…)

  从 MySQL 8.0.20 开始,删除了BNL算法,使用了Hash Join 算法替代。

执行流程

我们以下面官方例子为准:

SELECT  
	given_name,country_name
FROM persons
JOIN countries
ON persons.country_id = countries.country_id;

hash join 分为两个阶段; build 构建阶段和 probe 探测阶段。

build 构建

在构建阶段,MySQL使用Join字段作为哈希表Key,在内存中构建Hash 表。

正常情况MySQL会选择结果集较小(以字节为单位,而不是行数)的去构建。比如上面会对 countries.country_id 进行 hash 计算:hash(countries.country_id) 然后将值放入内存中 hash table 的相应位置。将所有行存储在哈希表中后,构建阶段就完成了。

probe 探测阶段

在探测阶段,MySQL逐行遍历被驱动表,然后计算join条件的hash值,并在hash表中查找,如果匹配,则输出给客户端,否则跳过。所有内表记录遍历完,则整个过程就结束了。

比如上面遍历persons 表中每行数据,然后取出Join字段的值进行 hash 计算;hash(persons.country_id),然后去内存中 hash table 中进行查找,匹配成功会发送给客户端。

内存中hash表的大小是由参数join_buffer_size 控制的,但是,如果构建hash table 内存大于可用内存,会发生什么情况?

  当内存在build构建阶段变满时,服务器会将其余的构建写入磁盘上的多个文件中。同时会设置文件的数量,确保最大的文件的大小与join_buffer_size一致。

每行数据写入哪个块文件是通过计算 join 属性的哈希来确定的。请注意,在下图中,使用的哈希函数与内存中生成阶段使用的哈希函数不同。我们稍后会明白为什么

在探测阶段,服务器对于persons 表每一行数据使用同样的hash算法进行分区。这样,我们就可以确定两个输入之间的匹配行将位于同一对文件中。

探测阶段完成后,开始从磁盘读取文件。首先会将build构建阶段的第一个文件,也就是下图中的左边的文件,加载到内存中哈希表中。这就解释了为什么希望最大的文件大小与内存一致,接着读取在探测阶段生成的文件,下图中右边的文件,在内存哈希表中进行匹配,就像之前内存操作一样。处理第一个文件后,移动到下一块文件,继续,直到处理完所有文件。

到这里也明白了,如果我们对两个操作使用相同的哈希函数,那么在将构建文件加载到哈希表中时,我们会得到一个非常糟糕的哈希表,因为同一个文件中的所有行都将哈希为相同的值。

如何使用

MySQL 8.0.20之前,使用 EXPLAIN FORMAT=tree 来查看是否将使用Hash Join算法。

mysql> EXPLAIN   FORMAT=tree  SELECT
	u.`name`,
	u.user_code,
	o.order_name
FROM
	`user` u
	JOIN `order` o ON u.user_code = o.user_code;
+-------------------------------------------------------------+
| EXPLAIN
+-------------------------------------------------+
| -> Inner hash join (u.user_code = o.user_code)  (cost=7.50 rows=7)
    -> Table scan on o  (cost=0.05 rows=9)
    -> Hash
        -> Table scan on u  (cost=0.95 rows=7)
+-----------------------------------------------------------+
1 row in set (0.04 sec)

时间复杂度分析

整体上对驱动表遍历1次(驱动表有M行记录),被驱动表遍历1次(被驱动表有N行记录)。 因此它的时间复杂度为:O(M+N)O(M+N)O(M+N)。它通常比嵌套循环算法(NLJ)更有效,尤其是在其中一个结果集可以完全放入join_buffer内存的情况下。

MySQL JOIN 优化

NLJ算法优化

为了优化Nested-Loop Join的性能,尽可能减少 Join 语句中的 Nested Loop 的循环总次数,就是让驱动表的结果集尽可能的小。对于很多表的关联通过应用层拆分成多个语句然后再拼接查询结果更方便, 而且性能也不会差。

join的时候明确知道哪张表是小表的时候,可以用straight_join写法固定连接驱动方式

BNL算法优化

使用Block Nested-Loop Join(BNL)算法时,可能会对被驱动表做多次扫描,占用磁盘IO资源,并且需要执行M∗NM*NM∗N次对比但是会占用过多的CPU资源。

优化的常见做法是,给被驱动表的join字段加上索引,把BNL算法转成NLJ算法。

无法设置索引的情况可以通过设置join_buffer_size参数来控制Join Buffer的大小,以减少分段查询次数

Hash Join算法优化

Hash Join算法在从 MySQL 8.0.18 以后才会用到,也是为了替代上面的BNJ算法。

当哈希表所需的内存量超过join_buffer_size大小,会使用磁盘的文件进行处理,所以增加 join_buffer_size值避免生成文件可以极大提升查询速度。

以上就是一文详解MySQL—Join的使用优化的详细内容,更多关于MySQL Join使用优化的资料请关注我们其它相关文章!

(0)

相关推荐

  • 浅析Mysql Join语法以及性能优化

    一.Join语法概述 join 用于多表中字段之间的联系,语法如下: 复制代码 代码如下: ... FROM table1 INNER|LEFT|RIGHT JOIN table2 ON conditiona table1:左表:table2:右表. JOIN 按照功能大致分为如下三类: INNER JOIN(内连接,或等值连接):取得两个表中存在连接匹配关系的记录. LEFT JOIN(左连接):取得左表(table1)完全记录,即是右表(table2)并无对应匹配记录. RIGHT JOIN

  • MYSQL Left Join优化(10秒优化到20毫秒内)

    目录 [功能背景] [原始的SQL] [原始的SQL分析] [分析步骤] [优化后的SQL] [优化的SQL分析] 结合工作中的内容和大家分享一次Left Jon优化的过程,希望能给同学们新的思路. [功能背景]     我们需要按照用户订单号和商户号统计出购买的商品数量和售后的商品数量.涉及到的表和关系见下图: 很不幸工程师在起初进行表结构设计的时候没有在商户订单表中记录下购买的商品总数,在商户订单的售后单中也没记录下售后的商品数量. [原始的SQL] select o.no,s_order.

  • MySQL中(JOIN/ORDER BY)语句的查询过程及优化方法

    在MySQL查询语句过程和EXPLAIN语句基本概念及其优化中介绍了EXPLAIN语句,并举了一个慢查询例子: 可以看到上述的查询需要检查1万多记录,并且使用了临时表和filesort排序,这样的查询在用户数快速增长后将成为噩梦. 在优化这个语句之前,我们先了解下SQL查询的基本执行过程: 1.应用通过MySQL API把查询命令发送给MySQL服务器,然后被解析 2.检查权限.MySQL optimizer进行优化,经过解析和优化后的查询命令被编译为CPU可运行的二进制形式的查询计划(quer

  • 探究MySQL优化器对索引和JOIN顺序的选择

    本文通过一个案例来看看MySQL优化器如何选择索引和JOIN顺序.表结构和数据准备参考本文最后部分"测试环境".这里主要介绍MySQL优化器的主要执行流程,而不是介绍一个优化器的各个组件(这是另一个话题). 我们知道,MySQL优化器只有两个自由度:顺序选择:单表访问方式:这里将详细剖析下面的SQL,看看MySQL优化器如何做出每一步的选择. explain select * from employee as A,department as B where A.LastName = '

  • MySQL查询优化:连接查询排序limit(join、order by、limit语句)介绍

    不知道有没有人碰到过这样恶心的问题:两张表连接查询并limit,SQL效率很高,但是加上order by以后,语句的执行时间变的巨长,效率巨低. 情况是这么一个情况:现在有两张表,team表和people表,每个people属于一个team,people中有个字段team_id. 下面给出建表语句: 复制代码 代码如下: create table t_team ( id int primary key, tname varchar(100) ); create table t_people (

  • MySQL优化之使用连接(join)代替子查询

    使用连接(JOIN)来代替子查询(Sub-Queries) MySQL从4.1开始支持SQL的子查询.这个技术可以使用SELECT语句来创建一个单列的查询结果,然后把这个结果作为过滤条件用在另一个查询中.例如,我们要将客户基本信息表中没有任何订单的客户删除掉,就可以利用子查询先从销售信息表中将所有发出订单的客户ID取出来,然后将结果传递给主查询,如下所示: DELETE FROM customerinfo WHERE CustomerID NOT in (SELECT CustomerID FR

  • 一文详解MySQL中数据表的外连接

    目录 为什么要使用外连接 外连接简介 左连接与右连接 外连接练习① 外连接练习② 该章节的内容为多表连接查询的外连接,因为 MySQL 是关系型数据库,数据是拆分重组在多个数据表里面的.所以我们势必要从多个数据表中提取数据,通过 SQL 语句的内连接与外连接就能够实现多表查询了.这部分内容是需要我们重点学习的,学习的过程中会穿插多种的案例来强化对表连接的语法的运用. 为什么要使用外连接 在解释为什么使用 “外连接” 之前,先来看一个记录.(如下:) 针对表中的张三没有所属的部门编号,我们暂且将他

  • 一文详解MySQL Binlog日志与主从复制

    目录 1. Binlog日志的介绍 2. 主从复制 2.1 主从复制的流程 2.2 GTID 2.3 复制模型 2.4 MGR模式 2.5 并行回放 1. Binlog日志的介绍 Binlog是Binary log的缩写,即二进制日志.Binlog主要有三个作用:持久化时将随机IO转化为顺序IO,主从复制以及数据恢复.本文重点主从复制相关的问题. Binlog日志由一个索引文件与很多日志文件组成,每个日志文件由魔数以及事件组成,每个日志文件都会以一个Rotate类型的事件结束. 对于每个事件,都

  • 一文详解MySQL主从同步原理

    目录 1. MySQL主从同步实现方式 2. MySQL主从同步的作用 一主多从架构 双主多从架构 3. 主动同步的原理 4. 主从同步延迟问题 主从同步延迟的原因有哪些? 主从同步延迟的解决方案? 5. 如何提升主从同步性能 从库开启多线程复制 修改同步模式,改为异步 修改从库Bin Log配置 知识点总结 1. MySQL主从同步实现方式 MySQL主从同步是基于Bin Log实现的,而Bin Log记录的是原始SQL语句. Bin Log共有三种日志格式,可以binlog_format配置

  • 一文详解MySQL不同隔离级别都使用什么锁

    目录 说透 MySQL 锁机制 事务隔离级别 MySQL 锁类型 读未提交 读已提交 可重复读 总结 在上篇文章,我们聊了「MySQL 啥时候会用表锁,啥时候用行锁」这个问题.在文章中,我们还留了一个问题,即:如果查询或更新时的数据特别多,是否从行锁会升级为表锁? 此外,还有朋友留言说到:不同的隔离级别可能会用不同的锁,可以结合隔离级别来聊聊.其实上面虽然是两个问题,但如果你把不同隔离级别下的加锁问题搞清楚了,那么第一个问题自然也清楚了. 今天,就让我带着大家来聊聊不同隔离级别下,都会使用什么锁

  • 详解MySQL索引原理以及优化

    前言 本文是美团一位大佬写的,还不错拿出来和大家分享下,代码中嵌套在html中sql语句是java框架的写法,理解其sql要执行的语句即可. 背景 MySQL凭借着出色的性能.低廉的成本.丰富的资源,已经成为绝大多数互联网公司的首选关系型数据库.虽然性能出色,但所谓"好马配好鞍",如何能够更好的使用它,已经成为开发工程师的必修课,我们经常会从职位描述上看到诸如"精通MySQL"."SQL语句优化"."了解数据库原理"等要求.我

  • 详解mysql 使用left join添加where条件的问题分析

    当前需求: 有group和factor两张表,一个group对应多个factor,现在想查询有效的group和对应的有效的factor,两个表都有isDel逻辑删除标志. 最开始的错误写法一: SELECT g.*,f.* FROM groups g LEFT JOIN factor f ON f.groupId = g.id where g.isDel=0 and f.isDel=0 LEFT JOIN 关键字会从左表 (table_name1) 那里返回所有的行,即使在右表 (table_n

  • 一文详解PHP连接MySQL数据库的三种方式

    目录 1.MySQL扩展 2.mysqli扩展 3.PDO扩展 知识点补充 PHP与MySQL的连接有三种API接口,分别是:PHP的MySQL扩展 .PHP的mysqli扩展 .PHP数据对象(PDO). 1.MySQL扩展 PHP 的 MySQL 扩展是设计开发允许 PHP 应用与 MySQL 数据库交互的早期扩展.MySQL 扩展提供了一个面向过程的接口,由于不支持后期MySQL服务端提供的一些特性.且太古老,又不安全,所以已被后来的 mysqli 完全取代: 使用方式如下 //自 PHP

  • 详解MySQL中EXPLAIN解释命令及用法讲解

    1,情景描述:同事教我在mysql中用explain,于是查看了一番返回内容的含义 2,现就有用处的内容做如下记录: 1,explain显示了mysql如何使用索引来处理select语句以及连接表.可以帮助选择更好的索引和写出更优化的查询语句. 使用方法,在select语句前加上explain就可以了: explain select count(DISTINCT uc_userid) as user_login from user_char_daily_gameapp_11 where uc_d

  • 详解MySQL 慢查询

    查询mysql的操作信息 show status -- 显示全部mysql操作信息 show status like "com_insert%"; -- 获得mysql的插入次数; show status like "com_delete%"; -- 获得mysql的删除次数; show status like "com_select%"; -- 获得mysql的查询次数; show status like "uptime";

  • 详解MySQL 数据库范式

    前言: 关于数据库范式,时常有听说过,一直没有详细去了解.一般数据库书籍或数据库课程会介绍范式相关内容,范式也经常出现在数据库考试题目中.不清楚你是否对范式有比较清晰的了解呢?本篇文章我们一起来学习下数据库范式吧. 1.数据库范式简介 为了建立冗余较小.结构合理的数据库,设计数据库时必须遵循一定的规则.在关系型数据库中这种规则就称为范式.范式是符合某一种设计要求的总结.要想设计一个结构合理的关系型数据库,必须满足一定的范式. 范式的英文名称是 Normal Form ,简称 NF .它是英国人

随机推荐