一. SQL优化--JOIN算法

1.1. JOIN 写法对比


(root@localhost) 17:51:15 [mytest]> select * from a;
+------+------+
| id | num |
+------+------+
| a | 5 |
| b | 10 |
| c | 15 |
| d | 10 |
| e | 99 |
+------+------+
5 rows in set (0.00 sec) (root@localhost) 17:51:20 [mytest]> select * from b;
+------+------+
| id | num |
+------+------+
| b | 5 |
| c | 15 |
| d | 20 |
| e | 99 |
+------+------+
4 rows in set (0.00 sec) --
-- 语法一
-- (root@localhost) 17:51:23 [mytest]> select * from a,b where a.id=b.id;
+------+------+------+------+
| id | num | id | num |
+------+------+------+------+
| b | 10 | b | 5 |
| c | 15 | c | 15 |
| d | 10 | d | 20 |
| e | 99 | e | 99 |
+------+------+------+------+
4 rows in set (0.00 sec) --
-- 语法二
-- (root@localhost) 17:51:42 [mytest]> select * from a inner join b where a.id=b.id;
+------+------+------+------+
| id | num | id | num |
+------+------+------+------+
| b | 10 | b | 5 |
| c | 15 | c | 15 |
| d | 10 | d | 20 |
| e | 99 | e | 99 |
+------+------+------+------+
4 rows in set (0.00 sec) --
-- 语法三
-- (root@localhost) 17:52:42 [mytest]> select * from a join b where a.id=b.id;
+------+------+------+------+
| id | num | id | num |
+------+------+------+------+
| b | 10 | b | 5 |
| c | 15 | c | 15 |
| d | 10 | d | 20 |
| e | 99 | e | 99 |
+------+------+------+------+
4 rows in set (0.00 sec) (root@localhost) 17:55:31 [mytest]> explain select * from a,b where a.id=b.id;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| 1 | SIMPLE | b | NULL | ALL | NULL | NULL | NULL | NULL | 4 | 100.00 | NULL |
| 1 | SIMPLE | a | NULL | ALL | NULL | NULL | NULL | NULL | 5 | 20.00 | Using where; Using Join Buffer (Block Nested Loop) |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
2 rows in set, 1 warning (0.00 sec) (root@localhost) 17:55:48 [mytest]> explain select * from a inner join b where a.id=b.id;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| 1 | SIMPLE | b | NULL | ALL | NULL | NULL | NULL | NULL | 4 | 100.00 | NULL |
| 1 | SIMPLE | a | NULL | ALL | NULL | NULL | NULL | NULL | 5 | 20.00 | Using where; Using Join Buffer (Block Nested Loop) |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
2 rows in set, 1 warning (0.00 sec) (root@localhost) 17:56:06 [mytest]> explain select * from a join b where a.id=b.id;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| 1 | SIMPLE | b | NULL | ALL | NULL | NULL | NULL | NULL | 4 | 100.00 | NULL |
| 1 | SIMPLE | a | NULL | ALL | NULL | NULL | NULL | NULL | 5 | 20.00 | Using where; Using Join Buffer (Block Nested Loop) |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------------------------------------------+
2 rows in set, 1 warning (0.00 sec)

通过上述的EXPLAIN可以得知,三种JOIN 语法在执行性能效果上都是一样的。

2. JOIN的成本

  • 两张表的JOIN的过程:

上图的Fetch阶段是指当内表关联的列辅助索引时,但是需要访问表中的数据,那么这时就需要再访问主键索引才能得到数据的过程,不论表的存储引擎是InnoDB还是MyISAM,这都是无法避免的,只是MyISAM回表速度要快点,因为其辅助索引存放的就是指向记录的指针,而InnoDB索引组织表,需要再次通过索引查找才能定位数据

Fetch阶段也不是必须存在的,如果是聚集索引链接,那么直接就能得到数据,无需回表,也就没有Fetch这个阶段。另外,上述给出了两张表之间的Join过程,多张表的Join就是继续上述这个过程。

  • 回表:在找到二级索引后,得到对应的主键,再回到聚集索引中找主键对应的记录

接着计算两张表Join的成本,这里有下列几种概念:

外表的扫描次数,记为O。通常外表的扫描次数都是1,即Join时扫描一次驱动表的数据即可Join
内表的扫描次数,记为I。根据不同Join算法,内表的扫描次数不同
读取表的记录数,记为R。根据不同Join算法,读取记录的数量可能不同
Join的比较次数,记为M。根据不同Join算法,比较次数不同
回表的读取记录的数,记为F。若Join的是辅助索引,可能需要回表取得最终的数据

评判Join算法是否优劣,就是查看上述这些操作的开销是否比较小。当然还要考虑I/O的访问方式顺序还是随机,总之Join的调优也是门艺术,并非想象的那么简单。

3. JOIN算法

  • nsted_loop join

    • simple nested-loop join
    • index nested-loop join
    • block nested-loop join
  • classic Hash Join
    • Only support in MariaDB
  • bached key access join
    • from MySQL 5.6
    • from MariaDB 5.5

3.1. simple nested loop join

  • simple nested_loog join外表(驱动表)中的每一条记录与内表中的记录进行判断

  • 其算法解析如下:

For each row r in R do  						-- 扫描R表
Foreach row s in S do -- 扫描S表
If r and s satisfy the join condition -- 如果r和s满足join条件
Then output the tuple -- 那就输出结果集
  • 假设外表的记录数为R,内表的记录数位S
  1. R 表,该表只扫描了一次;
  2. S 表,该表扫面了count(R) 次;
  3. 该方法相当于是一个笛卡尔积,实际上数据库不会使用该算法;
  4. 根据上面对于JOIN算法的评判标准来看,SNLJ的开销如下表所示:

3.2. index nested loop join

  • index nested_loop join 由于内表上有索引,将外表扫描一次,内表是通过查找内表相应的索引的方式,和外表的记录进行匹配。
For each row r in R do              -- 扫描R表
lookup s in S index -- 查询S表的索引(固定3~4次IO,B+树高度)
if found s == r -- 如果 r匹配了索引s
Then output the tuple -- 输出结果集
  1. S 表上有索引
  2. 扫描R表,将R表的记录和S表中的索引进行匹配
  3. R表上可以没有索引
  4. 优化器倾向使用记录数少的表作为外表(又称驱动表)
  5. 如果数据量大,index nested loop join的成本也是高的,尤其是在二级索引的情况下,需要大量的回表操作
  6. 因为索引查询的成本是比较固定的,故优化器都倾向于使用记录数少的表作为外表。故index nested_loop join的算法成本如下表所示:

上表Smatch表示通过索引找到匹配的记录数量。通过索引可以大幅降低内表的Join的比较次数,每次比较1条外表的记录,其实就是一次indexlookup(索引查找),而每次index lookup的成本就是树的高度,即IndexHeight

index nested_loop join的算法的效率能否达到用户的预期呢?

其实如果是通过表的主键索引进行Join,即使是大数据量的情况下,index nested_loop join的效率亦是相当不错的。因为索引查找的开销非常小,并且访问模式也是顺序的(假设大多数聚集索引的访问都是比较顺序的)。

大部分人诟病MySQL的index nested_loop join慢,主要是因为在进行Join的时候可能用到的索引并不主键的聚集索引,而是辅助索引,这时index nested_loop join的过程又需要多一步Fetch的过程,而且这个过程开销会相当的大:

由于访问的是辅助索引,如果查询需要访问聚集索引上的列,那么必要需要进行回表取数据,看似每条记录只是多了一次回表操作,但这才是index nested_loop join算法最大的弊端。

  • 首先,辅助索引的index lookup是比较随机I/O访问操作
  • 其次,根据index lookup再进行回表又是一个随机的I/O操作。所以说,index nested_loop join最大的弊端是其可能需要大量的离散操作,这在SSD出现之前是最大的瓶颈。而即使SSD的出现大幅提升了随机的访问性能,但是对比顺序I/O,其还是慢了很多,依然不在一个数量级上。

3.3. block nested loop join

block nested loop join 将外表中的需要join匹配的列(不是完整的记录)暂时保存在一块内存( Join Buffer )中,让后将这块内存中的数据内表进行匹配(内表只扫描一次)

  • block nested loop join 可被用于联接的是ALL,index,range的类型
For each tuple r in R do
store used columns as p from R in Join Buffer # 将部分或者全部R的记录保存到 Join Buffer中,记为p
For each tuple s in S do
If p and s satisfy the join condition # p 与 s满足join条件
Then output the tuple # 输出为结果集
block nested loop join 与simple nested loop join 相比,多了一个Join Buffer
mysql> show variables like "%join%buffer%";
+------------------+-----------+
| Variable_name | Value |
+------------------+-----------+
| join_buffer_size | 134217728 | -- 128M,默认是256K
+------------------+-----------+
1 row in set (0.00 sec)

Join Buffer 用的不是Buffer Pool中的内存,而是线程级别的内存。

可以通过explain查看执行计划,并通过join条件字段的大小,来预估 join_buffer_size 的大小。

注意:

增大join_buffer_size 进行优化的前提是没有使用index ,如果使用了index,根本不会使用block nested join算法

3.3.1 Join Buffer缓存的对象

Join Buffer缓存的对象是所有参与查询的列都会保存到Join Buffer,而不是只有Join的列.

比如下面的SQL语句,假设没有索引,需要使用到Join Buffer进行链接:

SELECT a.col3 FROM a,b
WHERE a.col1 = b.col2
AND a.col2 > … AND b.col2 = …

假设上述SQL语句的外表是a,内表是b,那么存放在Join Buffer中的列是所有参与查询的列,在这里就是(a.col1,a.col2,a.col3)。

通过上面的介绍,我们现在可以得到内表的扫描次数为:

Scaninner_table = (Rn * used_column_size) / join_buffer_size + 1可以预估需要分配的Join Buffer大小,然后尽量使得内表的扫描次数尽可能的少,最优的情况是只扫描内表一次。

3.3.2 Join Buffer的分配

需要牢记的是,Join Buffer是在Join之前就进行分配,并且每次Join就需要分配一次Join Buffer,所以假设有N张表参与Join,每张表之间通过Block Nested-Loop Join,那么总共需要分配N-1个Join Buffer,这个内存容量是需要DBA进行考量的。

  • Join Buffer可分为以下两类:

    • regular Join Buffer
    • incremental Join Buffer

regular Join Buffer是指Join Buffer缓存的是所有参与查询的列, 如果第一次使用Join Buffer,必然使用的是regular Join Buffer

incremental Join Buffer中的Join Buffer缓存的是当前使用的列,以及之前使用Join Buffer的指针。在多次进行Join的操作时,这样可以极大减少Join Buffer对于内存开销的需求。

此外,对于NULL类型的列,其实不需要存放在Join Buffer中,而对于VARCHAR类型的列,也是仅需最小的内存即可,而不是以CHAR类型在Join Buffer中保存。最后,从MySQL 5.6版本开始,对于Outer Join也可以使用Join Buffer。

3.3.3 Block Nested-Loop Join总结

Block Nested-Loop Join极大的避免了内表的扫描次数,如果Join Buffer可以缓存外表的数据,那么内表的扫描仅需一次,这和Hash Join非常类似。但是Block Nested-Loop Join依然没有解决的是Join比较的次数,其仍然通过Join判断式进行比较。综上所述,到目前为止各Join算法的成本比较如下所示:

3.4. MariaDB中的Hash Join算法

MySQL 目前不支持Hash Join

由上图可知, MariaDB 中的Hash Join 算法是在Block Join 基础之上,根据Join Buffer中的对象创建哈希表,内表通过哈希算法进行查找,减少内外表扫描的次数,提升Join的性能

  • MariaDB中的Hash Join问题是,优化器默认不会去选择Hash Join算法

    • set join_cache_level = 4;
    • set optimizer_switch='join_cache_hashed=on';

设置两个变量后,MariaDB将强制使用hash算法,无法智能判断

3.5. batched key access join

通过上图可以知道,在index join的基础上,增加MRR的功能,先对索引进行排序,然后批量获取聚集索引中的记录,由于使用了MRR的功能,所以默认该join算法也是不会被使用到的

  • set optimizer_switch='mrr_cost_based=off';
-- 方法一
mysql> SET optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on'; -- 在session中打开BKA特性
mysql> explain SELECT * FROM part, lineitem WHERE l_partkey=p_partkey AND p_retailprice>3000\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: part
partitions: NULL
type: ALL
possible_keys: PRIMARY
key: NULL
key_len: NULL
ref: NULL
rows: 195802
filtered: 33.33
Extra: Using where
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: lineitem
partitions: NULL
type: ref
possible_keys: i_l_suppkey_partkey,i_l_partkey
key: i_l_partkey
key_len: 5
ref: dbt3.part.p_partkey
rows: 27
filtered: 100.00
Extra: Using Join Buffer (Batched Key Access) -- 使用了BKA
2 rows in set, 1 warning (0.00 sec)
-- 方法二
mysql> SET optimizer_switch='mrr=on,mrr_cost_based=on,batched_key_access=off'; -- 在session中关闭BKA特性
mysql> explain SELECT /*+ BKA(lineitem)*/ * FROM part, lineitem WHERE l_partkey=p_partkey AND p_retailprice>2050\G -- 使用/*+ BKA(tablename)*/ 的语法,强制使用BKA特性
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: part
partitions: NULL
type: ALL
possible_keys: PRIMARY
key: NULL
key_len: NULL
ref: NULL
rows: 195802
filtered: 33.33
Extra: Using where
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: lineitem
partitions: NULL
type: ref
possible_keys: i_l_suppkey_partkey,i_l_partkey
key: i_l_partkey
key_len: 5
ref: dbt3.part.p_partkey
rows: 27
filtered: 100.00
Extra: Using Join Buffer (Batched Key Access) -- 使用了BKA
2 rows in set, 1 warning (0.00 sec)

二. MRR补充

(root@localhost) 10:35:54 [employees]> show variables like "%optimizer_switch%"\G;
*************************** 1. row ***************************
Variable_name: optimizer_switch
Value: 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
1 row in set (0.01 sec) ERROR:
No query specified (root@localhost) 10:36:05 [employees]> explain select * from employees where hire_date >= '1996-01-01'\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: range
possible_keys: idx_date
key: idx_date
key_len: 3
ref: NULL
rows: 41036
filtered: 100.00
Extra: Using index condition
1 row in set, 1 warning (0.00 sec) (root@localhost) 10:36:06 [employees]> explain select /*+ MRR(employees)*/ * from employees where hire_date >= '1996-01-01'\G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: employees
partitions: NULL
type: range
possible_keys: idx_date
key: idx_date
key_len: 3
ref: NULL
rows: 41036
filtered: 100.00
Extra: Using index condition; Using MRR --使用MRR
1 row in set, 1 warning (0.00 sec) ERROR:
No query specified (root@localhost) 10:36:46 [employees]>

如果强制开启MRR,那在某些SQL语句下,性能可能会变差;因为MRR需要排序,假如排序的时间超过直接执行的时间,那性能就会降低。

optimizer_switch 可以是全局的,也可以是会话级的

[参考MySQL Join算法与调优白皮书]

022:SQL优化--JOIN算法的更多相关文章

  1. sql优化 表连接join方式

        sql优化核心 是数据库中 解析器+优化器的工作,我觉得主要有以下几个大方面:1>扫表的方法(索引非索引.主键非主键.书签查.索引下推)2>关联表的方法(三种),关键是内存如何利用 ...

  2. paip.sql索引优化----join 代替子查询法

    paip.sql索引优化----join 代替子查询法 作者Attilax ,  EMAIL:1466519819@qq.com 来源:attilax的专栏 地址:http://blog.csdn.n ...

  3. MySQL Nested-Loop Join算法学习

    不知不觉的玩了两年多的MySQL,发现很多人都说MySQL对比Oracle来说,优化器做的比较差,其实某种程度上来说确实是这样,但是毕竟MySQL才到5.7版本,Oracle都已经发展到12c了,今天 ...

  4. 关于join算法的四篇文章

    MySQL Join算法与调优白皮书(一) MySQL Join算法与调优白皮书(二) MySQL Join算法与调优白皮书(三) MySQL Join算法与调优白皮书(四) MariaDB Join ...

  5. Spark SQL概念学习系列之Spark SQL 优化策略(五)

    查询优化是传统数据库中最为重要的一环,这项技术在传统数据库中已经很成熟.除了查询优化, Spark SQL 在存储上也进行了优化,从以下几点查看 Spark SQL 的一些优化策略. (1)内存列式存 ...

  6. SQL优化的四个方面,缓存,表结构,索引,SQL语句

    一,缓存 数据库属于 IO 密集型的应用程序,其主要职责就是数据的管理及存储工作.而我们知道,从内存中读取一个数据库的时间是微秒级别,而从一块普通硬盘上读取一个IO是在毫秒级别,二者相差3个数量级.所 ...

  7. 大型系统开发sql优化总结(转)

    Problem Description: 1.每个表的结构及主键索引情况 2.每个表的count(*)记录是多少 3.对于创建索引的列,索引的类型是什么?count(distinct indexcol ...

  8. 智能SQL优化工具--SQL Optimizer for SQL Server(帮助提升数据库应用程序性能,最大程度地自动优化你的SQL语句 )

    SQL Optimizer for SQL Server 帮助提升数据库应用程序性能,最大程度地自动优化你的SQL语句 SQL Optimizer for SQL Server 让 SQL Serve ...

  9. HASH JOIN算法

    哈希连接(HASH JOIN) 前文提到,嵌套循环只适合输出少量结果集.如果要返回大量结果集(比如返回100W数据),根据嵌套循环算法,被驱动表会扫描100W次,显然这是不对的.看到这里你应该明白为 ...

随机推荐

  1. 关于block和inline元素的float

    CSS float 浮动属性 本篇主要介绍float属性:定义元素朝哪个方向浮动. 目录 1. 页面布局方式:介绍文档流.浮动层以及float属性. 2. float:left :介绍float为 l ...

  2. 虚拟机中centos7与物理主机通讯

    本地物理机 WIN命令行:ipconfig 查看网络配置 在物理机的网络配置--> 配置VMnet8 打开VMware  编辑虚拟机设置,选择自定义NAT模式(VMnat8) 编辑->虚拟 ...

  3. 【干货】Kaggle 数据挖掘比赛经验分享(mark 专业的数据建模过程)

    简介 Kaggle 于 2010 年创立,专注数据科学,机器学习竞赛的举办,是全球最大的数据科学社区和数据竞赛平台.笔者从 2013 年开始,陆续参加了多场 Kaggle上面举办的比赛,相继获得了 C ...

  4. iOS-AFNetworking3.0上传大量(1000张)图片到服务器

    背景: 最近项目要做上传图片功能,图片必须是高清的,所以不让压缩,上传图片是大量的,比如几百张,这个如果是用afn,将图片直接for循环加入到formData里会出现一个问题,临时变量太多,导致内存紧 ...

  5. SSH服务及其扩展(sshpass和expect)

    SSH服务及其扩展(sshpass和expect) Linux SSH服务一共包含三个工具:ssh.scp.sftp [远程连接及执行命令] 语法:ssh -p端口 账号@IP 命令 参数说明:-o ...

  6. weight decay (权值衰减)

    http://blog.sina.com.cn/s/blog_890c6aa30100z7su.html 在机器学习或者模式识别中,会出现overfitting,而当网络逐渐overfitting时网 ...

  7. Git+GitHub+SaltStack

    本文主要介绍日常工作中会经常使用到的一些工具软件. [ 01 ]SaltStack简明教程 [ 02 ]Git 中文教程 Git详解之一 Git起步 Git详解之二 Git基础  Git详解之三 Gi ...

  8. WIN7下配置和使用解压缩版MYSQL

    最近mysql出了新的GA版本——mysql5.6.11,此版本windows64位下只有解压缩版,于是在win7上进行了配置.期间碰到了一些问题,在此记录一下. 一.环境 操作系统:WIN764位 ...

  9. Codeforces 620E New Year Tree【线段树傻逼题】

    LINK 题目大意 给你一棵树 让你支持子树染色,子树查询颜色个数,颜色数<=60, 节点数<=4e5 思路 因为颜色数很少,考虑状态压缩变成二进制 然后直接在dfs序上用线段树维护就可以 ...

  10. 《DSP using MATLAB》示例Example7.11

    代码: M = 45; As = 60; n = [0:1:M-1]; beta = 0.1102*(As - 8.7) %beta = 0.1102*(As - 8.7) + 0.3 w_kai = ...