learn.lianglianglee.com/专栏/MySQL实战45讲/35 join语句怎么优化?.md.html
2022-05-11 18:57:05 +08:00

1267 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>35 join语句怎么优化.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/MySQL实战45讲/00 开篇词 这一次让我们一起来搞懂MySQL.md.html">00 开篇词 这一次让我们一起来搞懂MySQL.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/01 基础架构一条SQL查询语句是如何执行的.md.html">01 基础架构一条SQL查询语句是如何执行的.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/02 日志系统一条SQL更新语句是如何执行的.md.html">02 日志系统一条SQL更新语句是如何执行的.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/03 事务隔离:为什么你改了我还看不见?.md.html">03 事务隔离:为什么你改了我还看不见?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/04 深入浅出索引(上).md.html">04 深入浅出索引(上).md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/05 深入浅出索引(下).md.html">05 深入浅出索引(下).md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/06 全局锁和表锁 :给表加个字段怎么有这么多阻碍?.md.html">06 全局锁和表锁 :给表加个字段怎么有这么多阻碍?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/07 行锁功过:怎么减少行锁对性能的影响?.md.html">07 行锁功过:怎么减少行锁对性能的影响?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/08 事务到底是隔离的还是不隔离的?.md.html">08 事务到底是隔离的还是不隔离的?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/09 普通索引和唯一索引,应该怎么选择?.md.html">09 普通索引和唯一索引,应该怎么选择?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/10 MySQL为什么有时候会选错索引.md.html">10 MySQL为什么有时候会选错索引.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/11 怎么给字符串字段加索引?.md.html">11 怎么给字符串字段加索引?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/12 为什么我的MySQL会“抖”一下.md.html">12 为什么我的MySQL会“抖”一下.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/13 为什么表数据删掉一半,表文件大小不变?.md.html">13 为什么表数据删掉一半,表文件大小不变?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/14 count()这么慢,我该怎么办?.md.html">14 count()这么慢,我该怎么办?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/15 答疑文章(一):日志和索引相关问题.md.html">15 答疑文章(一):日志和索引相关问题.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/16 “order by”是怎么工作的.md.html">16 “order by”是怎么工作的.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/17 如何正确地显示随机消息?.md.html">17 如何正确地显示随机消息?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/18 为什么这些SQL语句逻辑相同性能却差异巨大.md.html">18 为什么这些SQL语句逻辑相同性能却差异巨大.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/19 为什么我只查一行的语句,也执行这么慢?.md.html">19 为什么我只查一行的语句,也执行这么慢?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/20 幻读是什么,幻读有什么问题?.md.html">20 幻读是什么,幻读有什么问题?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/21 为什么我只改一行的语句,锁这么多?.md.html">21 为什么我只改一行的语句,锁这么多?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/22 MySQL有哪些“饮鸩止渴”提高性能的方法.md.html">22 MySQL有哪些“饮鸩止渴”提高性能的方法.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/23 MySQL是怎么保证数据不丢的.md.html">23 MySQL是怎么保证数据不丢的.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/24 MySQL是怎么保证主备一致的.md.html">24 MySQL是怎么保证主备一致的.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/25 MySQL是怎么保证高可用的.md.html">25 MySQL是怎么保证高可用的.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/26 备库为什么会延迟好几个小时?.md.html">26 备库为什么会延迟好几个小时?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/27 主库出问题了,从库怎么办?.md.html">27 主库出问题了,从库怎么办?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/28 读写分离有哪些坑?.md.html">28 读写分离有哪些坑?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/29 如何判断一个数据库是不是出问题了?.md.html">29 如何判断一个数据库是不是出问题了?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/30 答疑文章(二):用动态的观点看加锁.md.html">30 答疑文章(二):用动态的观点看加锁.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/31 误删数据后除了跑路,还能怎么办?.md.html">31 误删数据后除了跑路,还能怎么办?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/32 为什么还有kill不掉的语句.md.html">32 为什么还有kill不掉的语句.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/33 我查这么多数据,会不会把数据库内存打爆?.md.html">33 我查这么多数据,会不会把数据库内存打爆?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/34 到底可不可以使用join.md.html">34 到底可不可以使用join.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/MySQL实战45讲/35 join语句怎么优化.md.html">35 join语句怎么优化.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/36 为什么临时表可以重名?.md.html">36 为什么临时表可以重名?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/37 什么时候会使用内部临时表?.md.html">37 什么时候会使用内部临时表?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/38 都说InnoDB好那还要不要使用Memory引擎.md.html">38 都说InnoDB好那还要不要使用Memory引擎.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/39 自增主键为什么不是连续的?.md.html">39 自增主键为什么不是连续的?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/40 insert语句的锁为什么这么多.md.html">40 insert语句的锁为什么这么多.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/41 怎么最快地复制一张表?.md.html">41 怎么最快地复制一张表?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/42 grant之后要跟着flush privileges吗.md.html">42 grant之后要跟着flush privileges吗.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/43 要不要使用分区表?.md.html">43 要不要使用分区表?.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/44 答疑文章(三):说一说这些好问题.md.html">44 答疑文章(三):说一说这些好问题.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/45 自增id用完怎么办.md.html">45 自增id用完怎么办.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/我的MySQL心路历程.md.html">我的MySQL心路历程.md.html</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/结束语 点线网面一起构建MySQL知识网络.md.html">结束语 点线网面一起构建MySQL知识网络.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>35 join语句怎么优化</h1>
<p>在上一篇文章中,我和你介绍了 join 语句的两种算法,分别是 Index Nested-Loop Join(NLJ) 和 Block Nested-Loop Join(BNL)。</p>
<p>我们发现在使用 NLJ 算法的时候,其实效果还是不错的,比通过应用层拆分成多个语句然后再拼接查询结果更方便,而且性能也不会差。</p>
<p>但是BNL 算法在大表 join 的时候性能就差多了,比较次数等于两个表参与 join 的行数的乘积,很消耗 CPU 资源。</p>
<p>当然了,这两个算法都还有继续优化的空间,我们今天就来聊聊这个话题。</p>
<p>为了便于分析,我还是创建两个表 t1、t2 来和你展开今天的问题。</p>
<pre><code>create table t1(id int primary key, a int, b int, index(a));
create table t2 like t1;
drop procedure idata;
delimiter ;;
create procedure idata()
begin
declare i int;
set i=1;
while(i&lt;=1000)do
insert into t1 values(i, 1001-i, i);
set i=i+1;
end while;
set i=1;
while(i&lt;=1000000)do
insert into t2 values(i, i, i);
set i=i+1;
end while;
end;;
delimiter ;
call idata();
</code></pre>
<p>为了便于后面量化说明,我在表 t1 里,插入了 1000 行数据,每一行的 a=1001-id 的值。也就是说,表 t1 中字段 a 是逆序的。同时,我在表 t2 中插入了 100 万行数据。</p>
<h1>Multi-Range Read 优化</h1>
<p>在介绍 join 语句的优化方案之前我需要先和你介绍一个知识点Multi-Range Read 优化 (MRR)。这个优化的主要目的是尽量使用顺序读盘。</p>
<p>在[第 4 篇文章]中,我和你介绍 InnoDB 的索引结构时提到了“回表”的概念。我们先来回顾一下这个概念。回表是指InnoDB 在普通索引 a 上查到主键 id 的值后,再根据一个个主键 id 的值到主键索引上去查整行数据的过程。</p>
<p>然后,有同学在留言区问到,回表过程是一行行地查数据,还是批量地查数据?</p>
<p>我们先来看看这个问题。假设,我执行这个语句:</p>
<pre><code>select * from t1 where a&gt;=1 and a&lt;=100;
</code></pre>
<p>主键索引是一棵 B+ 树,在这棵树上,每次只能根据一个主键 id 查到一行数据。因此,回表肯定是一行行搜索主键索引的,基本流程如图 1 所示。</p>
<p><img src="assets/1761edbd7734276ae0a213af3cdd3311.jpg" alt="img" /></p>
<p>图 1 基本回表流程</p>
<p>如果随着 a 的值递增顺序查询的话id 的值就变成随机的,那么就会出现随机访问,性能相对较差。虽然“按行查”这个机制不能改,但是调整查询的顺序,还是能够加速的。</p>
<p><strong>因为大多数的数据都是按照主键递增顺序插入得到的,所以我们可以认为,如果按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能。</strong></p>
<p>这,就是 MRR 优化的设计思路。此时,语句的执行流程变成了这样:</p>
<ol>
<li>根据索引 a定位到满足条件的记录将 id 值放入 read_rnd_buffer 中 ;</li>
<li>将 read_rnd_buffer 中的 id 进行递增排序;</li>
<li>排序后的 id 数组,依次到主键 id 索引中查记录,并作为结果返回。</li>
</ol>
<p>这里read_rnd_buffer 的大小是由 read_rnd_buffer_size 参数控制的。如果步骤 1 中read_rnd_buffer 放满了,就会先执行完步骤 2 和 3然后清空 read_rnd_buffer。之后继续找索引 a 的下个记录,并继续循环。</p>
<p>另外需要说明的是,如果你想要稳定地使用 MRR 优化的话,需要设置<code>set optimizer_switch=&quot;mrr_cost_based=off&quot;</code>。(官方文档的说法,是现在的优化器策略,判断消耗的时候,会更倾向于不使用 MRR把 mrr_cost_based 设置为 off就是固定使用 MRR 了。)</p>
<p>下面两幅图就是使用了 MRR 优化后的执行流程和 explain 结果。</p>
<p><img src="assets/d502fbaea7cac6f815c626b078da86c7.jpg" alt="img" /></p>
<p>图 2 MRR 执行流程</p>
<p><img src="assets/a513d07ebaf1ae044d44391c89bc6432.png" alt="img" /></p>
<p>图 3 MRR 执行流程的 explain 结果</p>
<p>从图 3 的 explain 结果中,我们可以看到 Extra 字段多了 Using MRR表示的是用上了 MRR 优化。而且,由于我们在 read_rnd_buffer 中按照 id 做了排序,所以最后得到的结果集也是按照主键 id 递增顺序的,也就是与图 1 结果集中行的顺序相反。</p>
<p>到这里,我们小结一下。</p>
<p><strong>MRR 能够提升性能的核心</strong>在于,这条查询语句在索引 a 上做的是一个范围查询(也就是说,这是一个多值查询),可以得到足够多的主键 id。这样通过排序以后再去主键索引查数据才能体现出“顺序性”的优势。</p>
<h1>Batched Key Access</h1>
<p>理解了 MRR 性能提升的原理,我们就能理解 MySQL 在 5.6 版本后开始引入的 Batched Key Access(BKA) 算法了。这个 BKA 算法,其实就是对 NLJ 算法的优化。</p>
<p>我们再来看看上一篇文章中用到的 NLJ 算法的流程图:</p>
<p><img src="assets/10e14e8b9691ac6337d457172b641a3d.jpg" alt="img" /></p>
<p>图 4 Index Nested-Loop Join 流程图</p>
<p>NLJ 算法执行的逻辑是:从驱动表 t1一行行地取出 a 的值,再到被驱动表 t2 去做 join。也就是说对于表 t2 来说每次都是匹配一个值。这时MRR 的优势就用不上了。</p>
<p>那怎么才能一次性地多传些值给表 t2 呢?方法就是,从表 t1 里一次性地多拿些行出来,一起传给表 t2。</p>
<p>既然如此,我们就把表 t1 的数据取出来一部分,先放到一个临时内存。这个临时内存不是别人,就是 join_buffer。</p>
<p>通过上一篇文章,我们知道 join_buffer 在 BNL 算法里的作用,是暂存驱动表的数据。但是在 NLJ 算法里并没有用。那么,我们刚好就可以复用 join_buffer 到 BKA 算法中。</p>
<p>如图 5 所示,是上面的 NLJ 算法优化后的 BKA 算法的流程。</p>
<p><img src="assets/31d85666542b9cb0b47a447a8593a47e.jpg" alt="img" /></p>
<p>图 5 Batched Key Access 流程</p>
<p>图中,我在 join_buffer 中放入的数据是 P1~P100表示的是只会取查询需要的字段。当然如果 join buffer 放不下 P1~P100 的所有数据,就会把这 100 行数据分成多段执行上图的流程。</p>
<p>那么,这个 BKA 算法到底要怎么启用呢?</p>
<p>如果要使用 BKA 优化算法的话,你需要在执行 SQL 语句之前,先设置</p>
<pre><code>set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';
</code></pre>
<p>其中,前两个参数的作用是要启用 MRR。这么做的原因是BKA 算法的优化要依赖于 MRR。</p>
<h1>BNL 算法的性能问题</h1>
<p>说完了 NLJ 算法的优化,我们再来看 BNL 算法的优化。</p>
<p>我在上一篇文章末尾,给你留下的思考题是,使用 Block Nested-Loop Join(BNL) 算法时,可能会对被驱动表做多次扫描。如果这个被驱动表是一个大的冷数据表,除了会导致 IO 压力大以外,还会对系统有什么影响呢?</p>
<p>在[第 33 篇文章]中,我们说到 InnoDB 的 LRU 算法的时候提到,由于 InnoDB 对 Bufffer Pool 的 LRU 算法做了优化,即:第一次从磁盘读入内存的数据页,会先放在 old 区域。如果 1 秒之后这个数据页不再被访问了,就不会被移动到 LRU 链表头部,这样对 Buffer Pool 的命中率影响就不大。</p>
<p>但是,如果一个使用 BNL 算法的 join 语句,多次扫描一个冷表,而且这个语句执行时间超过 1 秒,就会在再次扫描冷表的时候,把冷表的数据页移到 LRU 链表头部。</p>
<p>这种情况对应的,是冷表的数据量小于整个 Buffer Pool 的 3/8能够完全放入 old 区域的情况。</p>
<p>如果这个冷表很大,就会出现另外一种情况:业务正常访问的数据页,没有机会进入 young 区域。</p>
<p>由于优化机制的存在,一个正常访问的数据页,要进入 young 区域,需要隔 1 秒后再次被访问到。但是,由于我们的 join 语句在循环读磁盘和淘汰内存页,进入 old 区域的数据页,很可能在 1 秒之内就被淘汰了。这样,就会导致这个 MySQL 实例的 Buffer Pool 在这段时间内young 区域的数据页没有被合理地淘汰。</p>
<p>也就是说,这两种情况都会影响 Buffer Pool 的正常运作。</p>
<p><strong>大表 join 操作虽然对 IO 有影响,但是在语句执行结束后,对 IO 的影响也就结束了。但是,对 Buffer Pool 的影响就是持续性的,需要依靠后续的查询请求慢慢恢复内存命中率。</strong></p>
<p>为了减少这种影响,你可以考虑增大 join_buffer_size 的值,减少对被驱动表的扫描次数。</p>
<p>也就是说BNL 算法对系统的影响主要包括三个方面:</p>
<ol>
<li>可能会多次扫描被驱动表,占用磁盘 IO 资源;</li>
<li>判断 join 条件需要执行 M*N 次对比M、N 分别是两张表的行数),如果是大表就会占用非常多的 CPU 资源;</li>
<li>可能会导致 Buffer Pool 的热数据被淘汰,影响内存命中率。</li>
</ol>
<p>我们执行语句之前,需要通过理论分析和查看 explain 结果的方式,确认是否要使用 BNL 算法。如果确认优化器会使用 BNL 算法,就需要做优化。优化的常见做法是,给被驱动表的 join 字段加上索引,把 BNL 算法转成 BKA 算法。</p>
<p>接下来,我们就具体看看,这个优化怎么做?</p>
<h1>BNL 转 BKA</h1>
<p>一些情况下,我们可以直接在被驱动表上建索引,这时就可以直接转成 BKA 算法了。</p>
<p>但是,有时候你确实会碰到一些不适合在被驱动表上建索引的情况。比如下面这个语句:</p>
<pre><code>select * from t1 join t2 on (t1.b=t2.b) where t2.b&gt;=1 and t2.b&lt;=2000;
</code></pre>
<p>我们在文章开始的时候,在表 t2 中插入了 100 万行数据,但是经过 where 条件过滤后,需要参与 join 的只有 2000 行数据。如果这条语句同时是一个低频的 SQL 语句,那么再为这个语句在表 t2 的字段 b 上创建一个索引就很浪费了。</p>
<p>但是,如果使用 BNL 算法来 join 的话,这个语句的执行流程是这样的:</p>
<ol>
<li>把表 t1 的所有字段取出来,存入 join_buffer 中。这个表只有 1000 行join_buffer_size 默认值是 256k可以完全存入。</li>
<li>扫描表 t2取出每一行数据跟 join_buffer 中的数据进行对比,
<ul>
<li>如果不满足 t1.b=t2.b则跳过</li>
<li>如果满足 t1.b=t2.b, 再判断其他条件,也就是是否满足 t2.b 处于 [1,2000] 的条件,如果是,就作为结果集的一部分返回,否则跳过。</li>
</ul>
</li>
</ol>
<p>我在上一篇文章中说过,对于表 t2 的每一行,判断 join 是否满足的时候,都需要遍历 join_buffer 中的所有行。因此判断等值条件的次数是 1000*100 万 =10 亿次,这个判断的工作量很大。</p>
<p><img src="assets/92fbdbfc35da3040396401250cb33f60.png" alt="img" /></p>
<p>图 6 explain 结果</p>
<p><img src="assets/d862bc3e88305688df2c354a4b26809c.png" alt="img" /></p>
<p>图 7 语句执行时间</p>
<p>可以看到explain 结果里 Extra 字段显示使用了 BNL 算法。在我的测试环境里,这条语句需要执行 1 分 11 秒。</p>
<p>在表 t2 的字段 b 上创建索引会浪费资源,但是不创建索引的话这个语句的等值条件要判断 10 亿次,想想也是浪费。那么,有没有两全其美的办法呢?</p>
<p>这时候,我们可以考虑使用临时表。使用临时表的大致思路是:</p>
<ol>
<li>把表 t2 中满足条件的数据放在临时表 tmp_t 中;</li>
<li>为了让 join 使用 BKA 算法,给临时表 tmp_t 的字段 b 加上索引;</li>
<li>让表 t1 和 tmp_t 做 join 操作。</li>
</ol>
<p>此时,对应的 SQL 语句的写法如下:</p>
<pre><code>create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
insert into temp_t select * from t2 where b&gt;=1 and b&lt;=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);
</code></pre>
<p>图 8 就是这个语句序列的执行效果。</p>
<p><img src="assets/a80cdffe8173fa0fd8969ed976ac6ac7.png" alt="img" /></p>
<p>图 8 使用临时表的执行效果</p>
<p>可以看到,整个过程 3 个语句执行时间的总和还不到 1 秒,相比于前面的 1 分 11 秒,性能得到了大幅提升。接下来,我们一起看一下这个过程的消耗:</p>
<ol>
<li>执行 insert 语句构造 temp_t 表并插入数据的过程中,对表 t2 做了全表扫描,这里扫描行数是 100 万。</li>
<li>之后的 join 语句,扫描表 t1这里的扫描行数是 1000join 比较过程中,做了 1000 次带索引的查询。相比于优化前的 join 语句需要做 10 亿次条件判断来说,这个优化效果还是很明显的。</li>
</ol>
<p>总体来看,不论是在原表上加索引,还是用有索引的临时表,我们的思路都是让 join 语句能够用上被驱动表上的索引,来触发 BKA 算法,提升查询性能。</p>
<h1>扩展 -hash join</h1>
<p>看到这里你可能发现了,其实上面计算 10 亿次那个操作,看上去有点儿傻。如果 join_buffer 里面维护的不是一个无序数组,而是一个哈希表的话,那么就不是 10 亿次判断,而是 100 万次 hash 查找。这样的话,整条语句的执行速度就快多了吧?</p>
<p>确实如此。</p>
<p>这,也正是 MySQL 的优化器和执行器一直被诟病的一个原因:不支持哈希 join。并且MySQL 官方的 roadmap也是迟迟没有把这个优化排上议程。</p>
<p>实际上,这个优化思路,我们可以自己实现在业务端。实现流程大致如下:</p>
<ol>
<li><code>select * from t1;</code>取得表 t1 的全部 1000 行数据,在业务端存入一个 hash 结构,比如 C++ 里的 set、PHP 的数组这样的数据结构。</li>
<li><code>select * from t2 where b&gt;=1 and b&lt;=2000;</code> 获取表 t2 中满足条件的 2000 行数据。</li>
<li>把这 2000 行数据,一行一行地取到业务端,到 hash 结构的数据表中寻找匹配的数据。满足匹配的条件的这行数据,就作为结果集的一行。</li>
</ol>
<p>理论上,这个过程会比临时表方案的执行速度还要快一些。如果你感兴趣的话,可以自己验证一下。</p>
<h1>小结</h1>
<p>今天,我和你分享了 Index Nested-Loop JoinNLJ和 Block Nested-Loop JoinBNL的优化方法。</p>
<p>在这些优化方法中:</p>
<ol>
<li>BKA 优化是 MySQL 已经内置支持的,建议你默认使用;</li>
<li>BNL 算法效率低,建议你都尽量转成 BKA 算法。优化的方向就是给被驱动表的关联字段加上索引;</li>
<li>基于临时表的改进方案,对于能够提前过滤出小数据的 join 语句来说,效果还是很好的;</li>
<li>MySQL 目前的版本还不支持 hash join但你可以配合应用端自己模拟出来理论上效果要好于临时表的方案。</li>
</ol>
<p>最后,我给你留下一道思考题吧。</p>
<p>我们在讲 join 语句的这两篇文章中,都只涉及到了两个表的 join。那么现在有一个三个表 join 的需求,假设这三个表的表结构如下:</p>
<pre><code>CREATE TABLE `t1` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
`c` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
create table t2 like t1;
create table t3 like t2;
insert into ... // 初始化三张表的数据
</code></pre>
<p>语句的需求实现如下的 join 逻辑:</p>
<pre><code>select * from t1 join t2 on(t1.a=t2.a) join t3 on (t2.b=t3.b) where t1.c&gt;=X and t2.c&gt;=Y and t3.c&gt;=Z;
</code></pre>
<p>现在为了得到最快的执行速度,如果让你来设计表 t1、t2、t3 上的索引,来支持这个 join 语句,你会加哪些索引呢?</p>
<p>同时,如果我希望你用 straight_join 来重写这个语句,配合你创建的索引,你就需要安排连接顺序,你主要考虑的因素是什么呢?</p>
<p>你可以把你的方案和分析写在留言区,我会在下一篇文章的末尾和你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/MySQL实战45讲/34 到底可不可以使用join.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/MySQL实战45讲/36 为什么临时表可以重名?.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709972d96cbe3d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>