MySql 技术内幕 (第5章 联接与集合操作)
-
mysql支持:
-
cross join (交叉联结)
-
inner join (内联结)
-
outer join (外联结)
-
其他
逻辑查询处理的前三个阶段,from on join;
5.1 联接查询
5.1.1 新旧查询语法
select * from demo, demo_new where demo.id = demo_new.id ; select * from demo inner join demo_new on demo.id = demo_new.id
mysql8 看上面的执行计划是相同的;
5.1.2 CROSS JOIN
全表连接,对两个表产生笛卡尔积,返回所有列的组合,m*n行的表;
5.1.3 INNER JOIN
INNER JOIN 首先产生笛卡尔儿积的虚拟表,再按照ON过滤条件进行数据的匹配操作。
INNER JOIN不添加外部行,这是和OUTER JOIN最大区别之一,所以过滤条件在ON和WHERE中是没有区别的
MYSQL INNER JOIN后不跟ON子句,就等于CROSS JOIN产生笛卡尔积(MYSQL中,INNER JOIN和CROSS JOIN是同义词)
5.1.4 OUTER JOIN
用户可以按照一些过滤条件匹配表之间的数据;
与inner join不同的是,在通过outer join 添加的保留表中存在未找到的匹配数据;
mysql 支持 left outer join 和 right outer join;
与inner 关键字一样,可以省略outer 关键字;
mysql数据库不支持 full outer join;
outer join应用逻辑查询的前三个步骤,
产生笛卡尔积;
应用on过滤器;
添加外部行;
保留表中未匹配数据用null填充;
可以使用USING来简化ON子句:
select * from demo left outer join demo_new USING (id);
5.1.5 NATURAL JOIN
自然联接 = INNER join + USING 的组合;
隐含作用是两个表中具有相同名称的列进行匹配;
NATURAL LEFT(RIGHT) JOIN = LEFT(RIGHT) JOIN +USING 组合
5.1.6 STRAIGHT_JOIN
① 并不是新的联结类型,而是用户对sql优化器的控制,等同于join;
通过STRAIGHT_JOIN,mysql会强制读取左边的表;
② 比如: a 表记录46条, b表 记录8条;
explain select * from a inner join b on a.id = b.id;
上面执行计划显示先选择右边b表数据,这样匹配数据少;
explain select * from a straight_join b on a.id = b.id;
straight_join 会强制使用左边的表进行匹配;
5.2 其他联接分类
5.2.1 SELF JOIN
SELF JOIN 是同一个表的两个实例之间的join操作(自己关联自己);
使用的时候需要用不同的别名;
场景: 比如员工-经理 这种层次机构问题可以关联查询;
5.2.2 NONEQUI JOIN
前面的都是EQUAL JOIN(等值联接),NONEQUI JOIN则是不等于联接;
主要是on 条件里面使用"等于" 之外的运算符
5.2.3 SEMI JOIN和ANTI SEMI JOIN
根据一个表中存在相关记录数据找到另一个表中相关数据的连接;
SEMI JOIN的方法有很多种,如内部联接,子查询,集合操作等。
相反的是ANTI SEMI JOIN ,它根据一个表中不存在的记而从另一一个表中记录返回,使用outer join 并过滤外部行,可以实现ANTI SEMI JOIN ;
5.3 多表联接
对于inner join 的多表联接查询,可以随便按排表的顺序,因为优化器会自动根据成本评估出访问表的顺序;
多表的inner join 的on条件可以放在一起;
5.4 滑动订单问题
create table monthlyorders ( ordermonth date null, ordernum int null ); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2010-02-01', 23); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2010-03-01', 26); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2010-04-01', 24); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2010-05-01', 27); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2010-06-01', 26); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2010-07-01', 32); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2010-08-01', 34); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2010-09-01', 30); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2010-10-01', 31); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2010-11-01', 32); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2010-12-01', 33); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2011-01-01', 31); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2011-02-01', 34); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2011-03-01', 34); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2011-04-01', 38); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2011-05-01', 39); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2011-06-01', 35); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2011-07-01', 49); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2011-08-01', 56); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2011-09-01', 55); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2011-10-01', 74); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2011-11-01', 75); INSERT INTO test.monthlyorders (ordermonth, ordernum) VALUES ('2011-12-01', 14);
滑动订单问题: 每个月返回上一年度(或季度等)的滑动订单数,即为每个月分N,返回从月份N-11到月份N的订单总数,这里,假设月份序列不存在间隔;
select date_format(a.ordermonth, '%Y%m') AS frommonth, date_format(b.ordermonth, '%Y%m') AS tomonth, sum(c.ordernum) as orders from monthlyorders a inner join monthlyorders b on date_add(a.ordermonth, interval 11 MONTH) = B.ordermonth inner join monthlyorders c on c.ordermonth between a.ordermonth and b.ordermonth group by a.ordermonth, b.ordermonth
这里的做法主要是自连接,结果如下;
5.5 联接算法
联接算法 是mysql数据库用于处理联接的物理策略;
目前mysql仅仅支持Nested-Loops Join算法;
mysql8中推出了hash join算法;
mysql根据不同的使用场合支持两种 Nested-Loops Join 算法, 一张是simple Nested-Loops Join(NLJ)算法,另一种是Block Nested-Loops Join (BNL)算法;
前置概念:
驱动表/外部表:
指定了联接条件时,满足查询条件的记录行数少的表为[ 驱动表 ], 未指定联接条件时,行数少的表为[驱动表](Important!);
忠告:如果你搞不清楚该让谁做驱动表、谁 join 谁,请让 MySQL 运行时自行判断
Join Type:
all: 这便是所谓的“全表扫描”。
index: 这种连接类型只是另外一种形式的全表扫描,只不过它的扫描顺序是按照索引的顺序。
range: 指的是有范围的索引扫描,相对于index的全索引扫描,它有范围限制,因此要优于index。
ref: 查找条件列使用了索引而且不为主键和unique。其实,意思就是虽然使用了索引,但该索引列的值并不唯一,有重复。
这样即使使用索引快速查找到了第一条数据,仍然不能停止,要进行目标值附近的小范围扫描;
但它的好处是它并不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内扫描。
const: 通常情况下,如果将一个主键放置到where后面作为条件查询,mysql优化器就能把这次查询优化转化为一个常量。
5.5.1 Simple Nested-Loops Join算法
从外部表中每次读取一条记录,然后将记录与内部表中的记录进行比较(all/index/range/ref),算法如下:
for each row r in R do For each row s in S do If r and s satisfy the join condition Then output the tuple<r,s>
R为外部表(Outer Table),S为内部表(Inner Table);
内部表的选择:
优化器一般情况下总是选择将联接列含有索引的表作为内部表;
如果两张表的连接列都有索引,并且索引高度相同,那么优化器会选择将记录少的表作为外部表。(因为内部表的扫描次数总是索引高度,与记录数无关)
pushed-down conditions优化
对于内部表的过滤条件,SQL优化器可能会将判断放在外部表中去,能过将外部表过滤掉相当多的一部分数据。
在INNER JOIN中可以使用pushed-down conditions的优化方式,但是不能直接在OUTER JOIN中使用该方式,因为有些不满足联接条件的记录会通过外部表行的方式再次添加到结果中。
5.5.2 Block Nested-Loops Join算法
Simple Nested-Loops Join算法在内层循环时,外部表的每行记录需要读取内部表一次, 在内部表的联接上有索引读的情况下,其扫描成本是O(Rn) ,如果没有索引扫描成本是O(Rn*Sn) ;
如果内部表记录比较多,则效率差;
Block Nested-Loops Join算法就是针对没有索引的联接情况设计的,使用Join Buffer(联接缓存)来减少内部表的循环读取次数。
例如,
Block Nested-Loops Join算法先对外部表每次读取10行记录(需要进行联接的)放入到Join Buffer中,然后在内部表中直接匹配这10行数据。
MySQL使用Join Buffer的原则如下:
-
系统变量join_buffer_size决定了Join Buffer的大小
-
Join Buffer可被用于联接是ALL、index和range的类型
-
每次联接使用一个Join Buffer,因此多表的联接可以使用多个Join Buffer
-
Join Buffer在联接发生之前进行分配,在SQL语句执行完后进行释放
-
Join Buffer只存储需要进行查询操作的相关列的数据,而不是整行的记录
但是在mysql8里面则显示的是 hash join;
5.5.3 Batched Key Access Join算法
加快JOIN的执行速度,有如下两种方式:
-
加快每次search-for-match操作的速度(索引)
-
search-for-match根据group来进行,减少内部表的访问次数(Block Nested-Loops Join)
第一种方式,通过添加索引方式,让优化器选择索引进行search-for-match;
第二种方式,采用Block Nested-Loops Join算法思想,使用额外一小部分Join buffer内存将外部表数据放入Join buffer,然后在内部表中根据group来进行search-for-match,以减少内部表的访问次数;
Batched Key Access Join算法 可以理解为group-index-lookup(组索引查询)
对于非主键索引的联接,Batched Key Access Join算法可以极大提高SQL的执行效率;
Batched Key Access Join算法工作步骤如下:
-
将外部表中相关列放入 Join Buffer中;
-
批量地将key(索引键值) 发送到 Multi-Read(MRR) 接口;
-
MRR通过收到的key,根据对应的rowid进行排序,然后在进行数据的读取操作;
-
返回结果集给客户端
Multi-Read(MRR) 接口主要是用来提高获取记录的效率;
总结:
Batched Key Access Join算法本质上还是Simple Nested-Loops Join算法,
其发生的条件为内部表上有索引,并且该索引为非主键的,
并且联接需要访问内部表主键上的索引;
这时 Batched Key Access Join调用 Multi-Read(MRR) 接口,批量地进行索引键的匹配和主键索引上获取数据的操作,以此来提高联接的执行效率;
5.5.4 Classic Hash Join算法
Classic Hash Join算法步骤:
同样使用 Join Buffer ,
先将外部表的数据放入 Join Buffer 中,
然后根据键值产生一张散列表, 这是第一个阶段,(build阶段);
随后读取内部表中的一条记录对应散列函数,将其和散列函数中的数据进行比较,这是第二个阶段,(probe阶段);
如果Join Buffer完全放的下外部表,则只需要扫描内部表一次,否则需要多次;
5.6 集合操作
5.6.1 集合操作的概述
联接操作是表之间的水平操作,因为该操作生成的虚拟表包含两个表的列;集合操作,一般视为垂直操作:
5.6.2 UNION DISTINCT和UNION ALL
-
UNION DISTINCT , DISTINCT 一般省略;
Mysql对 UNION DISTINCT实现方式如下:
a) 创建一张临时表,即虚拟表;
b) 对这张临时表的列添加唯一索引;
c) 将输入的数据插入临时表;
d) 返回临时表。
由于有唯一索引,对性能还是影响的,所以在确定没有重复数据时,最好是用UNION ALL。
-
UNION ALL
如果没有重复项,可以使用UNION ALL,效率比UNION DISTINCT高;
5.6.3 EXCEPT
可以分为 EXCEPT DISTINCT和 EXCEPT ALL;
EXCEPT DISTINCT 找出位于第一个输入中但不位于第二个输入中的行数据。
存在null值问题;
5.6.4 INTERSECT
返回在两个输入中都出现的行。
存在null值问题;
5.7 小结
省略
参考: https://www.jianshu.com/p/b9f44bedac1e
https://www.cnblogs.com/liangjf/p/10633946.html