一、关联查询优化
1.1、数据准备
- 分类表
CREATE TABLE IF NOT EXISTS `type`
(
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`id`)
);
- 向分类表中添加20条记录
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO type(card)
VALUES (FLOOR(1 + (RAND() * 20)));
- 图书表
CREATE TABLE IF NOT EXISTS `book`
(
`bookid` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`card` INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (`bookid`)
);
- 向图书表中添加20条记录
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
1.2、左外连接
1.2.1、当没有索引时
- SQL
EXPLAIN
SELECT SQL_NO_CACHE *
FROM `type`
LEFT JOIN book ON type.card = book.card;
- EXPLAIN结果 image.png
- 小结:
type表
和book表
全都是全表扫描
1.2.2、为被驱动表
添加索引
CREATE INDEX idx_book_card ON book (card);
- SQL
EXPLAIN
SELECT SQL_NO_CACHE *
FROM `type`
LEFT JOIN book ON type.card = book.card;
- EXPLAIN结果 image.png
- 小结:可以看到第二行的
book
变为了ref
,rows
也变成了优化比较明显。这是由于左连接特性决定的。LEFT JOIN
条件用于确定如何从右表(被驱动表)
搜索行,左表(驱动表)
一定都有,所以被驱动表是关键点,一定需要建立索引
。
1.2.2、为驱动表
添加索引
CREATE INDEX idx_type_card ON type (card);
- SQL
EXPLAIN
SELECT SQL_NO_CACHE *
FROM `type`
LEFT JOIN book ON type.card = book.card;
- EXPLAIN结果 image.png
- 小结:
驱动表
和被驱动表
都适用消息
1.2.3、删除被驱动表
索引
DROP INDEX idx_book_card ON book;
- SQL
EXPLAIN
SELECT SQL_NO_CACHE *
FROM `type`
LEFT JOIN book ON type.card = book.card;
- EXPLAIN image.png
1.3、采用内连接
1.3.1、采用内连接没有索引
- 删除所有索引
DROP INDEX idx_type_card ON type;
DROP INDEX idx_book_card ON book;
- SQL
EXPLAIN
SELECT SQL_NO_CACHE *
FROM type
INNER JOIN book ON type.card = book.card;
- EXPLAIN结果 image.png
1.3.2、为被驱动表 book
添加索引
CREATE INDEX idx_book_card ON book (card);
- SQL
EXPLAIN
SELECT SQL_NO_CACHE *
FROM type
INNER JOIN book ON type.card = book.card;
- EXPLAIN结果 image.png
1.3.2、为驱动表 type
添加索引
CREATE INDEX idx_type_card ON type (card);
- SQL
EXPLAIN
SELECT SQL_NO_CACHE *
FROM type
INNER JOIN book ON type.card = book.card;
- EXPLAIN image.png
- 小结:
驱动表
和被驱动表
更换了;对于内连接来说,查询优化器可以决定谁作为驱动表,谁作为被驱动表
1.3.3、删除被驱动表 type
索引
DROP INDEX idx_type_card ON type;
- SQL
EXPLAIN
SELECT SQL_NO_CACHE *
FROM type
INNER JOIN book ON type.card = book.card;
- EXPLAIN image.png
-
小结:
对于内连接来讲,如果表的连接条件中只能有一个字段有索引,则有索引的字段所在的表会被作为驱动表
1.3.4、当所以表中都为内连接字段添加索引时,而一个表数据量大时
- 向 book表中添加数据(20条数据)
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
INSERT INTO book(card)
VALUES (FLOOR(1 + (RAND() * 20)));
- SQL
EXPLAIN
SELECT SQL_NO_CACHE *
FROM type
INNER JOIN book ON type.card = book.card;
- EXPLAIN image.png
-
小结:
对于内连接来说,在两个表的连接条件都存在索引的情况下,会选择小表(数据量小)作为驱动表。“小表驱动大表”
二、JOIN 的底层原理
JOIN 方式连接多个表,本质就是各个表之间数据的
循环匹配
。MySQL5.5 版本之前,MySQL只支持一种表间关联方式,就是嵌套循环(Nested Loop Join)
。如果关联表的数据量很大,则JOIN关联的执行时间会非常长。在 MySQL5.5 以后的版本中,MySQL通过引入BNLJ
算法来优化嵌套执行
2.1、驱动表和被驱动表概念
-
驱动表
:就是主表 -
被驱动表
:从表、非驱动表
2.1.1、对于内连接
SELECT * FROM A JOIN B . . .
-
表A
一定是驱动表
吗?不一定,优化器会根据查询语句做优化,决定先查哪张表
。先查询的那张表就是驱动表
,反之就是被驱动表
。可以通过EXPLAIN
查看
2.1.1、对于外连接
SELECT * FROM A LEFT JOIN B . . .
或
SELECT * FROM B REGHT JOIN A . . .
- 数据准备
CREATE TABLE t_a
(
f1 INT,
f2 INT,
INDEX (f1)
);
CREATE TABLE t_b
(
f1 INT,
f2 INT
);
- 插入数据
- t_a
INSERT INTO t_a (f1, f2)
VALUES (1, 1),
(2, 2),
(3, 3),
(4, 4),
(5, 5),
(6, 6);
- t_b
INSERT INTO t_b (f1, f2)
VALUES
(3, 3),
(4, 4),
(5, 5),
(6, 6),
(7, 7),
(8, 8);
- 测试一
EXPLAIN
SELECT *
FROM t_a a
LEFT JOIN t_b b ON a.f1 = b.f1
WHERE a.f2 = b.f2;
- EXPLAIN image.png
-
小结:表
t_a
为被驱动表
-
查看
SHOW WARNINGS
优化器
SHOW WARNINGS\G;
- 优化器优化后SQL
把
LEFT JOIN
优化成了JOIN
SELECT `atguigudb2`.`a`.`f1` AS `f1`,
`atguigudb2`.`a`.`f2` AS `f2`,
`atguigudb2`.`b`.`f1` AS `f1`,
`atguigudb2`.`b`.`f2` AS `f2`
FROM `atguigudb2`.`t_a` `a`
JOIN `atguigudb2`.`t_b` `b`
WHERE ((`atguigudb2`.`a`.`f1` = `atguigudb2`.`b`.`f1`) AND
(`atguigudb2`.`a`.`f2` = `atguigudb2`.`b`.`f2`));
image.png
- 测试二
EXPLAIN
SELECT *
FROM t_a a
LEFT JOIN t_b b ON (a.f1 = b.f1) AND (a.f2 = b.f2);
- EXPLAIN image.png
-
使用
SHOW WARNINGS
查看优化器
SHOW WARNINGS\G;
image.png
- 优化器后SQL
SELECT `atguigudb2`.`a`.`f1` AS `f1`,
`atguigudb2`.`a`.`f2` AS `f2`,
`atguigudb2`.`b`.`f1` AS `f1`,
`atguigudb2`.`b`.`f2` AS `f2`
FROM `atguigudb2`.`t_a` `a`
LEFT JOIN `atguigudb2`.`t_b` `b` ON (((`atguigudb2`.`b`.`f2` = `atguigudb2`.`a`.`f2`) AND
(`atguigudb2`.`b`.`f1` = `atguigudb2`.`a`.`f1`)))
WHERE TRUE
- 测试三
EXPLAIN
SELECT *
FROM t_a a
JOIN t_b b ON a.f1 = b.f1
WHERE a.f2 = b.f2;
- EXPLAIN image.png
-
使用
SHOW WARNINGS
查看优化器
SHOW WARNINGS\G;
image.png
- 优化器
SELECT `atguigudb2`.`a`.`f1` AS `f1`,
`atguigudb2`.`a`.`f2` AS `f2`,
`atguigudb2`.`b`.`f1` AS `f1`,
`atguigudb2`.`b`.`f2` AS `f2`
FROM `atguigudb2`.`t_a` `a`
JOIN `atguigudb2`.`t_b` `b`
WHERE ((`atguigudb2`.`a`.`f1` = `atguigudb2`.`b`.`f1`) AND (`atguigudb2`.`a`.`f2` = `atguigudb2`.`b`.`f2`));
2.2、Simple Nested-Loop Join(简单嵌套循环连接)
计算逻辑图.png算法相当简单,从表A中取出一条数据1,遍历表B,将匹配到的数据放到
Result
。以此类推,驱动表A
中的每一条记录与被驱动表B
的记录进行判断
- 当表A数据为
100
条,表B数据1000
条计算,则A*B= 10万次 - 开销统计 Simple Nested-Loop Join - 开销统计.png
2.3、Index Nested-Loop Join(索引嵌套循环链接)
Index Nested-Loop Join
其优化的思路主要是为了减少内层表数据的匹配次数
,所以要求被驱动表
上必须有索引
才行。通过外层表匹配条件直接与内层表索引进行匹配
,避免和内层表的每条记录去进行比较,这样极大的减少了对内层表
的匹配次数。
-
Index Nested-Loop Join逻辑图
Index Nested-Loop Join - 逻辑图.png -
驱动表
中的每条记录通过被驱动表
的索引
进行访问,因为索引
查询的成本是比较固定的,故MySQL优化器都倾向于使用记录数少的表作为驱动表(外表)
- Index Nested-Loop Join开销统计 Index Nested-Loop Join开销统计.png
-
小结:如果
被驱动表
加索引,效率是非常高的,但如果索引不是主键索引,所以还得进行一次回表查询
。相比,被驱动表
的索引是主键索引
,效率会更高
2.4、Block Nested-Loop Join(块嵌套循环连接)
如果存在索引,那么会使用
Index
的方式进行Join
,如果Join
的列没有索引,被驱动表
要扫描的次数太多了。每次访问被驱动表
,其表中的记录都会被加载到内存
中,然后再从驱动表
中取一条与其匹配
,匹配结束后清除内存
,然后再从驱动表
中加载一条记录,然后把被驱动表
的记录再加载到内存
匹配,这样周而复始,大大增加了IO
的次数。为了减少被驱动表
的IO
次数,就出现了Block Nested-Loop Join
。
不再是逐条获取驱动表
的数据,而是一块一块的获取,引入了Join Buffer 缓冲区
,将驱动表
JOIN相关的部分数据列(大小受Join Buffer
的限制)缓存到Join Buffer
中,然后全表扫描被驱动表
,被驱动表
的每一条记录一次性和Join Buffer
中的所有驱动表
记录进行匹配(内存中操作),将简单嵌套循环中的多次比较合并成一次,降低了被驱动表
的访问频率
- Join Buffer 中缓存内容
缓存的不只是
关联表的列(f1)
,而是缓存的SELECT 后面需要输出的列
在一个有N
个JOIN关联的SQL中会分配N-1
个Join buffer
。所以查询的时候尽量减少不必要的查询字段,可以让Join buffer
中存放更多记录
- Block Nested-Loop Join逻辑图 Block Nested-Loop Join逻辑图.png
- 多缓存 image.png
- 开销统计 开销统计.png
-
参数设置
-
block_nested_loop
通过
SHOW VARIABLES LIKE '%optimizer_switch%';
查看block_nested_loop
状态,默认开启
SHOW VARIABLES LIKE '%optimizer_switch%';
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
- join_buffer_size
驱动表
能不能一次加载完,要看join_buffer
能不能存储所有的数据,默认情况下join_buffer_size=262144(256k)
。join_buffer_size
的最大值在32位系统
可以申请4G
,而在64位操作系统
下可以申请大于4G
的join_buffer
空间(64位 Windows 除外,其大值会被截断为4G
并发出警告)
SHOW VARIABLES LIKE '%join_buffer%';
- 小结
- 1、整体效率比较:
INLJ > BNLJ > SNLJ
- 2、永远用
小结果集(小表)
驱动大结果集(大表)
,本质就是减少外层循序的数据量
- 3、为
被驱动表
匹配的条件增加索引(减少内层表的循环匹配次数) - 4、增大
join buffer size
的大小(一次缓存的数据越多,那么内层包的扫表次数就越少) - 5、减少
驱动表
不必要的字段查询(字段越少,join buffer 所缓存的数据就越多)
- 1、整体效率比较:
2.5、Hash Join
从 MySQL 的8.0.20 版本开始将废弃
BNLJ
,因为从MySQL8.0.18版本开始就加入了Hash Join
默认都会使用Hash Join
- Nested Loop:对于被连接的数据子集较小的情况,
Nested Loop
是个较好的选择 - Hash Join:是做
大数据集连接
时的常用方式,优化器使用两个表中较小的表利用join key
在内存中建立散列表
,然后扫描较大的表并探测散列表,找出与Hash
表匹配的行- 这种方式使用于较小的表完全可以放于内存中的情况,这样总成本就是访问两个表的成本之和
- 在表很大的情况下并不能完全放入内存,这时优化器会将它分割成
若干不同的分区
,不能放入内存的部分就把该分区写入磁盘
的临时段,此时要求有较大的临时段从而尽量提高I/O
的性能 - 它能很好的工作于没有索引的
大表
和并行查询的环境中,并提供最好的性能。大多数人都说它是JOIN的重新升降机。hash join 只能应用于等值连接
这是youHash的特点决定的
image.png
2.6、小结
- 保证
被驱动表
的JOIN字段已经创建了索引
- 需要JOIN 的字段,
数据类型保持绝对一致
-
LEFT JOIN
时,选择小表作为驱动表
,大表作为被驱动表
。减少外层循环的次数 -
INNER JOIN
时,MySQL会自动将小结果集的表选为驱动表
。选择相信MySQL优化策略 - 能够直接多表关联的尽量直接关联,
不用子查询
。(减少查询的趟数) -
不建议使用子查询
,建议将子查询
SQL拆开结合程序多次查询,或使用 JOIN 来代替子查询 - 衍生表建不了索引
网友评论