MySQL---join驱动表的选择

本文详细介绍了SQL中的JOIN操作,包括INNER JOIN、LEFT JOIN和RIGHT JOIN,并探讨了在多表连接时如何选择驱动表。重点讨论了IndexNested-Loop Join和BlockNested-Loop Join两种算法,强调了小表作为驱动表的重要性,以减少扫描行数和提高查询效率。此外,还提到了join_buffer_size参数和内存判断次数在优化中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

join关键字

当业务需要从多个数据表中读取数据时,可以使用SQL语句中的连接(JOIN),在两个或多个数据表中查询数据。

JOIN 按照功能可分为三类:

  1. INNER JOIN(内连接,或等值连接):获取两个表中字段匹配关系的记录;
  2. LEFT JOIN(左连接):获取左表中的所有记录,即使在右表没有对应匹配的记录;
  3. RIGHT JOIN(右连接):与 LEFT JOIN 相反,用于获取右表中的所有记录,即使左表没有对应匹配的记录。

但是当有多张表的时候,驱动表该如何选择?

首先建一张表用来演示。

CREATE TABLE `t2` (
`id` int(11) NOTNULL,
`a` int(11) DEFAULTNULL,
`b` int(11) DEFAULTNULL,
PRIMARY KEY (`id`),
KEY `a` (`a`)
) ENGINE=InnoDB;

CREATE TABLE `t1` LIKE `t2`;
假设插入的数据是一一对应的以方面计算。

提示:文中用到的straight_join完全等同于inner join,但是从左到右实现强制多表的载入顺序,不同于inner join会进行优化。

IndexNested-Loop Join

对于SQLselect * from t1 straight_join t2 on (t1.a=t2.a);有以下执行流程:

  1. 从表t1中读入一行数据 R;
  2. 从数据行R中,取出a字段到表t2里去查找;
  3. 取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分;
  4. 重复执行步骤1到3,直到表t1的末尾循环结束。

我们称之为IndexNested-Loop Join算法,简称NLJ。

在这个流程里:

  1. 对驱动表t1做了全表扫描,这个过程需要扫描N行;
  2. 对于每一行R,根据a字段去表t2查找,走的是树搜索过程。由于我们构造的数据都是一一对应的,因此每次的搜索过程都只扫描一行,也是总共扫描M行;
  3. 所以整个执行流程,总扫描行数是N+M。

假设被驱动表的行数是M。每次在被驱动表查一行数据,要先搜索索引a,再搜索主键索引。每次搜索一棵树近似复杂度是以2为底的M的对数,记为log2M,所以在被驱动表上查一行的时间复杂度是 2*log2M。

当驱动表的行数是N,执行过程就要扫描驱动表N行,然后对于每一行,到被驱动表上匹配一次。因此整个执行过程,近似复杂度是 N+N2log M,很明显N对扫描行数的影响更大,因此应该让小表来做驱动表。(N扩大1000倍的话,扫描行数就会扩大1000倍;而M扩大1000倍,扫描行数扩大不到10倍。)

Block Nested-Loop Join

SQLselect * from t1 straight_join t2 on (t1.a=t2.b);由于表t2的字段b上没有索引,因此搜索时,每次到t2去匹配的时候,就要做一次全表扫描。假设驱动表行数是N,被驱动表行数是M,那就需要扫描行数是N*M。

我们将这个算法称为Simple Nested-Loop Join,但是MySQL没有使用这个算法,而是使用了另一个叫作BlockNested-Loop Join的算法,简称BNL。

这时候,被驱动表上没有可用的索引,算法的流程是这样的:

  1. 把表t1的数据读入线程内存join_buffer中,由于我们这个语句中写的是select *,因此是把整个表t1放入了内存;
  2. 扫描表t2,把表t2中的每一行取出来,跟join_buffer中的数据做对比,满足join条件的,作为结果集的一部分返回。

假设小表的行数是N,大表的行数是M,在这个算法里:

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

看样子和Simple Nested-Loop Join是一样的,但是Block Nested-Loop Join算法的判断是内存操作,速度上会快很多,性能也更好。

这里涉及到一个参数是join_buffer_size,join_buffer的大小是由join_buffer_size的大小控制的,默认256K。如果放不下的话,策略是分段放。

假设t1有100行,内存放不下,执行过程就变成了:

  1. 扫描表t1,顺序读取数据行放入join_buffer中,放完第80行join_buffer满了,继续第2步;
  2. 扫描表t2,把t2中的每一行取出来,跟join_buffer中的数据做对比,满足join条件的,作为结果集的一部分返回;
  3. 清空join_buffer;
  4. 继续扫描表t1,顺序读取最后的20行数据放入join_buffer中,继续执行第2步。

假设驱动表的数据行数是N,需要分K段才能完成算法流程,被驱动表的数据行数是M。这里的K不是常数,N越大K就会越大,因此把K表示为λ*N,λ的取值范围是(0,1)。

在这个算法的执行过程中:

  1. 扫描行数是 N+λ*N*M
  2. 内存判断 N*M次。

从内存判断次数看来不论哪个作为驱动表结果都是一样的,但是考虑到扫描行数,在M和N大小确定的情况下,N小一些,整个算式的结果会更小。所以使用小表作为驱动表会更好。

小表的概念

假设t1有100行,t2有1000行。

对于SQLselect * from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=50;join_buffer只需要放入t2的前50行。虽然t2比t1的行数多,但是这里“t2的前50行”是那个相对小的表,也就是“小表”。

select t1.b,t2.* from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=100select t1.b,t2.* from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=100

这两条SQL表t1只查字段b,因此如果把t1放到join_buffer中,则join_buffer中只需要放入b的值;表t2需要查所有的字段,因此如果把表t2放到join_buffer中的话,就需要放入三个字段id、a和b。

在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与join的各个字段的总数据量,数据量小的那个表就是小表

总结:

  1. 如果可以使用IndexNested-Loop Join算法,也就是说可以用上被驱动表上的索引,可以使用join连接,同时将小表作为驱动表。
  2. 如果使用Block Nested-Loop Join算法,扫描行数就会过多。尤其是在大表上的join操作,这样可能要扫描被驱动表很多次,会占用大量的系统资源。这种join连接要慎用。
  3. 不管使用哪一种,都尽量将小表作为驱动表!
<think>好的,我现在需要帮用户理解MySQL中两JOIN的关联逻辑。用户之前问过关于复合主键的问题,现在转向JOIN关联,可能需要基础的解释和示例。 首先,回顾之前的对话,用户对复合主键有了一定了解,现在可能是在实际查询中遇到了JOIN的问题,或者想优化查询性能。根据提供的引用,用户可能关心JOIN类型、驱动选择、索引使用以及优化技巧。 用户引用的资料提到了驱动选择、不同JOIN类型的区别(如LEFT JOIN vs INNER JOIN)、ON和WHERE条件的区别,以及字符集对索引的影响。这些点都需要在回答中涵盖。 用户需要的是关联逻辑的详细说明,可能包括执行步骤、示例和优化建议。需要确保回答结构清晰,结合示例代码和解释,同时引用提供的资料来增强可信度。 要注意使用中文,数学达式用$...$,独立公式用$$...$$,但这个问题可能不需要太多数学符号,主要是SQL语法和逻辑解释。需要检查LaTeX是否正确,比如名或字段是否用$包裹。 可能需要分步骤解释JOIN的过程,比如驱动选择、匹配过程、结果合并。然后通过不同类型的JOIN示例(INNER, LEFT, RIGHT)展示差异。最后给出优化建议,如索引、字符集一致、过滤条件位置等,引用提供的资料支持观点。 还要生成相关问题,考虑用户可能接下来会问的问题,比如驱动选择原理、索引失效原因、JOIN与子查询的比较等。</think>### MySQLJOIN关联逻辑详解 --- #### 一、JOIN核心逻辑 MySQLJOIN操作本质是通过**嵌套循环**实现: 1. **选择驱动**:优化器根据大小、索引、过滤条件选择驱动(小优先原则)[^1] 2. **逐行匹配**:遍历驱动的每一行,在被驱动中查找匹配记录 3. **结果合并**:根据JOIN类型(INNER/LEFT/RIGHT)决定是否保留未匹配记录 ```sql -- 示例结构 CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(50), dept_id INT, INDEX(dept_id) ); CREATE TABLE departments ( id INT PRIMARY KEY, dept_name VARCHAR(50) ); ``` --- #### 二、JOIN类型与执行流程对比 | JOIN类型 | 执行逻辑 | 伪代码示例 | |------------------|--------------------------------------------------------------------------|----------------------------------------------------------------------------| | **INNER JOIN** | 仅保留两匹配成功的记录 | `for user_row in users: if dept_match: output` | | **LEFT JOIN** | 保留左全部记录,右无匹配时填充NULL | `for user_row in users: output (user + dept or NULL)` | | **RIGHT JOIN** | 保留右全部记录,左无匹配时填充NULL | `for dept_row in departments: output (user or NULL + dept)` | | **FULL JOIN** | 通过UNION实现:`LEFT JOIN UNION RIGHT JOIN`[^2] | `(users LEFT JOIN depts) UNION (users RIGHT JOIN depts)` | --- #### 三、关键机制解析 1. **驱动选择规则**: -优先:数据量较小的作为驱动(默认行为)[^1] - 强制指定:可通过`STRAIGHT_JOIN`强制指定驱动顺序 ```sql SELECT * FROM users STRAIGHT_JOIN departments ON... ``` 2. **索引匹配机制**: - 最佳实践:确保关联字段(如`dept_id`)有索引且字符集一致[^5] ```sql -- 错误示例:字符集不一致导致索引失效 SELECT * FROM t3 JOIN t4 ON t3.a = t4.b; -- utf8 vs latin1[^5] ``` 3. **条件过滤优先级**: - `ON`子句:关联时过滤,影响匹配过程 - `WHERE`子句:关联后过滤,影响最终结果[^4] ```sql -- LEFT JOIN时右过滤应放在ON子句 SELECT * FROM users LEFT JOIN departments ON users.dept_id = departments.id AND departments.dept_name = 'IT' -- 正确过滤方式[^4] ``` --- #### 四、性能优化示例 ```sql -- 优化前(无索引) EXPLAIN SELECT * FROM users INNER JOIN departments ON users.dept_id = departments.id; -- 优化后(添加覆盖索引) ALTER TABLE departments ADD INDEX idx_dept_id_name(id, dept_name); ``` | 优化项 | 效果 | |------------------|------------------------------| | 索引匹配 | 减少全扫描,提升10倍速度 | | 小驱动 | 降低内存消耗30% | | 字符集统一 | 避免隐式转换导致的性能损失 | --- #### 五、特殊场景处理 **自关联查询**: ```sql -- 查询员工及其经理信息 SELECT e.name, m.name FROM employees e LEFT JOIN employees m ON e.manager_id = m.id ``` **多JOIN顺序**: ```sql SELECT * FROM table1 INNER JOIN table2 ON table1.id = table2.t1_id INNER JOIN table3 ON table2.id = table3.t2_id -- 执行顺序:table1 → table2 → table3[^3] ``` ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值