数据库优化
1. 分库分表
1.1 分库分表算法
待定
1.2 如何解决分库分表后的join问题
水平拓展后可能会导致库表发布到不同的MySQL实例上,这使得原有的单数据源关联查询变为多库源关联。
可以通过以下方法解决
绑定表(Binding Table)
指分片规则一致的一组分片表。 使用绑定表进行多表关联查询时,必须使用分片键进行关联,否则会出现笛卡尔积关联或跨库关联,从而影响查询效率。 例如:
t_order表和t_order_item表,均按照order_id分片,并且使用order_id进行关联,则此两张表互为绑定表关系。 绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。 举例说明,如果 SQL 为:1
SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);在不配置绑定表关系时,假设分片键 order_id 将数值 10 路由至第 0 片,将数值 11 路由至第 1 片,那么路由后的 SQL 应该为 4 条,它们呈现为笛卡尔积:
1
2
3
4
5
6
7SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);在配置绑定表关系,并且使用
order_id进行关联后,路由的 SQL 应该为 2 条:1
2
3SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);配置:
1
2
3
4
5
6
7
8
9rules: - !SHARDING tables: t_order: actualDataNodes: ds_${0..1}.t_order_${0..1} t_order_item: actualDataNodes: ds_${0..1}.t_order_item_${0..1} bindingTables: - t_order, t_order_item通过数据库中间件例如shardingsphere等工具实现,不过shardingsphere的联邦查询还不是很稳定,慎用。
将需要关联查询的数据直接冗余到分表上。
通过es等搜索引擎统一结构化存储提供外部检索查询。
1.3 分库分表扩容问题
我了解到两种
升级从库
这种方式就是通过升级从库为主库的方式实现数据迁移再改造hash的迁移方式。 举个例子,假设我们现在有两个分库,每个库中有一张分表,对应的分库分表算法即id%2得到库索引,然后将数据存入对应分库的分表中,例如我们现在要存储一个id为600w的数据,通过算法得到值为0,那么这条数据就存入分库0的tb表,对应的我们的从库也跟随db0做数据同步。
当现有主库数据已达到一定体量导致查询性能下降,我们可直接将各自的从库升级为主库,这是第一步。

完成升级从库为主库之后,db0对应的从库变为db2,此时这两个数据的数据表是重复的,因为我们将分表算法修改为id%4,所以我们需要基于这个算法清除冗余数据,即主库0删除id%4=2(这些是升级为主库的db2数据),db1删除id%4=3(这个是升级为主库的db3的数据),其余两个从库同理,自此完成算法和数据迁移的升级。
双写扩容
采用双写扩容策略,避免数据迁移。扩容前每个节点的数据,有一半要迁移至一个新增节点中,对应关系比较简单。具体操作如下(假设已有 2 个节点 A/B,要双倍扩容至 A/A2/B/B2 这 4 个节点):
无需停止应用服务器;
新增两个数据库 A2/B2 作为从库,设置主从同步关系为:A=>A2、B=>B2,直至主从数据同步完毕,保持实时同步
调整分片规则并使之生效:原 ID%2=0 => A 改为 ID%4=0 => A, ID%4=2 => A2;原 ID%2=1 => B 改为 ID%4=1 => B, ID%4=3 => B2。
解除数据库实例的主从同步关系,并使之生效;
此时,四个节点的数据都已完整,只是有冗余(多存了和自己配对的节点的那部分数据),择机清除即可(过后随时进行,不影响业务)。

1.4 分页查询
将单表进行水平维度的分库分表之后所导致的库源不一致,传统的limit查询就无法针对整个分布式维度的分页,此时我们不得不借助一些第三方工具类将库源抽象成一个维度进行实现分表查询,我们以sharding-jdbc为例,它的做法就说基于当前查询的页数n,到所有库源中查询前n页的数据并聚合,将分布式库源检索结果聚合成一个维度,然后进行排序从而得到实际上的第二页的数据并返回。
例如,我们的分库分表希望查到第二页的数据,按照sharding-jdbc的做法,它就会将所有库表的前2页的数据查出来,然后进行归并排序得到一个完整维度的前2页的数据,最后再筛选出第二页数据返回给用户:

ShardingSphere通过流式处理+归并排序的方式处理分页问题。
以分页查询 SELECT * FROM user ORDER BY id ASC LIMIT 10, 10(查询第2页的10条数据)为例,假设分片表为 user_0、user_1、user_2,数据分布如下:
| 分片表 | 数据(按 id 升序排列) |
|---|---|
| user_0 | 3, 6, 9, 12, 15, 18, 21, … |
| user_1 | 1, 4, 7, 10, 13, 16, 19, … |
| user_2 | 2, 5, 8, 11, 14, 17, 20, … |
- 初始化分片游标
- 每个分片执行改写后的 SQL(
LIMIT 0, 20),返回 按 id 升序排列的前 20 条数据。 - 为每个分片建立 游标迭代器(Cursor Iterator),初始指向分片结果的第 0 条数据。
- 每个分片执行改写后的 SQL(
- 优先队列(最小堆)中保存各分片游标的 当前最小值。
- 跳过偏移量
- 循环从队列中取出最小值,并更新对应分片的游标:
- 取出
1 (user_1),游标移动到下一条4,将4加入队列。 - 取出
2 (user_2),游标移动到5,将5加入队列。 - 重复此过程,直到跳过前 10 条数据。
- 取出
- 跳过过程的内存状态:
每次取出一个元素后,仅需更新对应分片的游标和队列,已跳过的数据不保留在内存中。
- 循环从队列中取出最小值,并更新对应分片的游标:
- 收集数据
- 继续从队列中取出最小值,但此时开始收集结果:
- 取出
11 (user_2),加入结果集,游标移动到14,将14加入队列。 - 取出
12 (user_0),加入结果集,游标移动到15,将15加入队列。 - 重复直到收集满 10 条数据。
- 取出
- 内存占用:
队列中始终仅维护当前各分片的候选值(3 个元素),而非全量数据。
- 继续从队列中取出最小值,但此时开始收集结果:
关键细节:
游标迭代器
分片结果集的代理:每个分片的查询结果通过数据库驱动(如 JDBC)的
ResultSet逐条获取,通过游标记录当前位置。懒加载机制:仅当需要获取下一条数据时,才从数据库网络流中读取,减少内存占用。
优先队列
排序依据:队列根据排序字段(如
id)的值排序,确保每次取出的候选值为当前全局最小(或最大)。动态更新:每次取出一个元素后,将对应分片的下一个候选值加入队列。
偏移量跳过优化
计数器机制:通过计数器累计跳过的数据量,达到
offset后立即停止跳过。数据丢弃:已跳过的数据不保留任何引用,允许 JVM 垃圾回收。
结果收集
提前终止:若某分片的游标已遍历完所有数据,该分片不再参与队列更新。
结果封装:最终结果集通过动态代理返回给用户,实现透明访问。
2. 读写分离
使用读写分离主要就是将写请求传到主节点,读请求传到从节点,所以这就涉及到主从同步问题。
2.1 主从延迟
数据同步有关的时间点主要包括以下三个:
- 主库 A 执行完成一个事务,写入 binlog,我们把这个时刻记为 T1;
- 之后传给备库 B,我们把备库 B 接收完这个 binlog 的时刻记为 T2;
- 备库 B 执行完成这个事务,我们把这个时刻记为 T3。
所谓主备延迟,就是同一个事务,在备库执行完成的时间和主库执行完成的时间之间的差值,也就是 T3-T1。
你可以在备库上执行 show slave status 命令,它的返回结果里面会显示 seconds_behind_master,用于表示当前备库延迟了多少秒。
seconds_behind_master 的计算方法是这样的:
- 每个事务的 binlog 里面都有一个时间字段,用于记录主库上写入的时间;
- 备库取出当前正在执行的事务的时间字段的值,计算它与当前系统时间的差值,得到
seconds_behind_master。
可以看到,其实 seconds_behind_master 这个参数计算的就是 T3-T1。所以,我们可以用 seconds_behind_master 来作为主备延迟的值,这个值的时间精度是秒。并且备库连接到主库的时候,会通过执行 SELECT UNIX_TIMESTAMP() 函数来获得当前主库的系统时间。如果这时候发现主库的系统时间与自己不一致,备库在执行 seconds_behind_master 计算的时候会自动扣掉这个差值。
导致主从延迟的原因:
从库压力大:
由于读写分离将读请求转发到了从节点上,但是忽略了对从库的压力控制,导致备库上的查询耗费了大量的 CPU 资源,影响了同步速度,造成主备延迟。
可以这样处理:
一主多从。除了备库外,可以多接几个从库,让这些从库来分担读的压力。
通过 binlog 输出到外部系统,比如 Hadoop 这类系统,让外部系统提供统计类查询的能力。
大事务
大事务这种情况很好理解。因为主库上必须等事务执行完成才会写入 binlog,再传给备库。所以,如果一个主库上的语句执行 10 分钟,那这个事务很可能就会导致从库延迟 10 分钟。比如说要一次性删除大量数据的情况下,可以考虑分批删除。
设置并发复制线程数
首先看主从复制的流程:
主库将数据库中数据的变化写入到 binlog
从库连接主库
从库会创建一个 I/O 线程向主库请求更新的 binlog
主库会创建一个 binlog dump 线程来发送 binlog ,从库中的 I/O 线程负责接收
从库的 I/O 线程将接收的 binlog 写入到 relay log 中。
从库的 SQL 线程读取 relay log 同步数据到本地(也就是再执行一遍 SQL )
为了提高效率,在后续的Mysql版本中,这里从库的SQL线程不再直接更新数据,只负责读取中转日志和分发事务。真正更新日志的,变成了 worker 线程。而 work 线程的个数,就是由参数 slave_parallel_workers 决定的。这个值不能设置得太高,毕竟备库还有可能要提供读查询,不能把 CPU 都吃光了。
2.2 主备切换

这是一个基本的一主多从结构。图中,虚线箭头表示的是主备关系,也就是 A 和 A’互为主备, 从库 B、C、D 指向的是主库 A。一主多从的设置,一般用于读写分离,主库负责所有的写入和一部分读,其他的读请求则由从库分担。
这里来看基于GTID的主从切换流程
GTID 的全称是 Global Transaction Identifier,也就是全局事务 ID,是一个事务在提交的时候生成的,是这个事务的唯一标识。它由两部分组成,格式是:GTID=server_uuid:gno
其中:
- server_uuid 是一个实例第一次启动时自动生成的,是一个全局唯一的值;
- gno 是一个整数,初始值是 1,每次提交事务的时候分配给这个事务,并加 1。
GTID 模式的启动也很简单,我们只需要在启动一个 MySQL 实例的时候,加上参数 gtid_mode=on 和 enforce_gtid_consistency=on 就可以了。
在 GTID 模式下,每个事务都会跟一个 GTID 一一对应。这个 GTID 有两种生成方式,而使用哪种方式取决于 session 变量 gtid_next 的值。
- 如果 gtid_next=automatic,代表使用默认值。这时,MySQL 就会把 server_uuid:gno 分配给这个事务。 a. 记录 binlog 的时候,先记录一行 SET @@SESSION.GTID_NEXT=‘server_uuid:gno’; b. 把这个 GTID 加入本实例的 GTID 集合。
- 如果 gtid_next 是一个指定的 GTID 的值,比如通过 set gtid_next=‘current_gtid’指定为 current_gtid,那么就有两种可能: a. 如果 current_gtid 已经存在于实例的 GTID 集合中,接下来执行的这个事务会直接被系统忽略; b. 如果 current_gtid 没有存在于实例的 GTID 集合中,就将这个 current_gtid 分配给接下来要执行的事务,也就是说系统不需要给这个事务生成新的 GTID,因此 gno 也不用加 1。
注意,一个 current_gtid 只能给一个事务使用。这个事务提交后,如果要执行下一个事务,就要执行 set 命令,把 gtid_next 设置成另外一个 gtid 或者 automatic。
这样,每个 MySQL 实例都维护了一个 GTID 集合,用来对应“这个实例执行过的所有事务”。

我们把当前时刻,实例 A’的 GTID 集合记为 set_a,实例 B 的 GTID 集合记为 set_b。接下来,我们就看看现在的主备切换逻辑。
我们在实例 B 上执行 start slave 命令,取 binlog 的逻辑是这样的:
- 实例 B 指定主库 A’,基于主备协议建立连接。
- 实例 B 把 set_b 发给主库 A’。
- 实例 A’算出 set_a 与 set_b 的差集,也就是所有存在于 set_a,但是不存在于 set_b 的 GTID 的集合,判断 A’本地是否包含了这个差集需要的所有 binlog 事务。 a. 如果不包含,表示 A’已经把实例 B 需要的 binlog 给删掉了,直接返回错误; b. 如果确认全部包含,A’从自己的 binlog 文件里面,找出第一个不在 set_b 的事务,发给 B;
- 之后就从这个事务开始,往后读文件,按顺序取 binlog 发给 B 去执行。
其实,这个逻辑里面包含了一个设计思想:在基于 GTID 的主备关系里,系统认为只要建立主备关系,就必须保证主库发给备库的日志是完整的。因此,如果实例 B 需要的日志已经不存在,A’就拒绝把日志发给 B。
这跟基于位点的主备协议不同。基于位点的协议,是由备库决定的,备库指定哪个位点,主库就发哪个位点,不做日志的完整性判断。
基于上面的介绍,我们再来看看引入 GTID 后,一主多从的切换场景下,主备切换是如何实现的。
由于不需要找位点了,所以从库 B、C、D 只需要分别执行 change master 命令指向实例 A’即可。
其实,严谨地说,主备切换不是不需要找位点了,而是找位点这个工作,在实例 A’内部就已经自动完成了。
2.3 处理过期读
强制走主库
强制走主库方案其实就是,将查询请求做分类。通常情况下,我们可以将查询请求分为这么两类:
- 对于必须要拿到最新结果的请求,强制将其发到主库上。比如,在一个交易平台上,卖家发布商品以后,马上要返回主页面,看商品是否发布成功。那么,这个请求需要拿到最新的结果,就必须走主库。
- 对于可以读到旧数据的请求,才将其发到从库上。在这个交易平台上,买家来逛商铺页面,就算晚几秒看到最新发布的商品,也是可以接受的。那么,这类请求就可以走从库。
主动判断主从是否有延迟
通过
seconds_behind_master每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0。如果还不等于 0 ,那就必须等到这个参数变为 0 才能执行查询请求。
对比位点确保主备无延迟

这是是一个 show slave status 结果的部分截图。
- Master_Log_File 和 Read_Master_Log_Pos,表示的是读到的主库的最新位点;
- Relay_Master_Log_File 和 Exec_Master_Log_Pos,表示的是备库执行的最新位点。
如果 Master_Log_File 和 Relay_Master_Log_File、Read_Master_Log_Pos 和 Exec_Master_Log_Pos 这两组值完全相同,就表示接收到的日志已经同步完成。
对比 GTID 集合确保主备无延迟
- Auto_Position=1 ,表示这对主备关系使用了 GTID 协议。
- Retrieved_Gtid_Set,是备库收到的所有日志的 GTID 集合;
- Executed_Gtid_Set,是备库所有已经执行完成的 GTID 集合。
如果这两个集合相同,也表示备库接收到的日志都已经同步完成。
可见,对比位点和对比 GTID 这两种方法,都要比判断 seconds_behind_master 是否为 0 更准确。
3. Join
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CREATE TABLE `t2` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `a` (`a`)
) ENGINE=InnoDB;
drop procedure idata;
delimiter ;;
create procedure idata()
begin
declare i int;
set i=1;
while(i<=1000)do
insert into t2 values(i, i, i);
set i=i+1;
end while;
end;;
delimiter ;
call idata();
create table t1 like t2;
insert into t1 (select * from t2 where id<=100)可以看到,这两个表都有一个主键索引 id 和一个索引 a,字段 b 上无索引。存储过程 idata() 往表 t2 里插入了 1000 行数据,在表 t1 里插入的是 100 行数据。
3.1 Index Nested-Loop Join
我们来看一下这个语句:
1
select * from t1 join t2 on (t1.a=t2.a);这个语句的执行流程是这样的:
- 从表 t1 中读入一行数据 R;
- 从数据行 R 中,取出 a 字段到表 t2 里去查找;
- 取出表 t2 中满足条件的行,跟 R 组成一行,作为结果集的一部分;
- 重复执行步骤 1 到 3,直到表 t1 的末尾循环结束。

在这个流程里:
- 对驱动表 t1 做了全表扫描,这个过程需要扫描 100 行;
- 而对于每一行 R,根据 a 字段去表 t2 查找,走的是树搜索过程。由于我们构造的数据都是一一对应的,因此每次的搜索过程都只扫描一行,也是总共扫描 100 行;
- 所以,整个执行流程,总扫描行数是 200。
在这个 join 语句执行过程中,驱动表是走全表扫描,而被驱动表是走树搜索。
假设被驱动表的行数是 M。每次在被驱动表查一行数据,要先搜索索引 a,再搜索主键索引。每次搜索一棵树近似复杂度是以 2 为底的 M 的对数,记为 log2M,所以在被驱动表上查一行的时间复杂度是 2*log2M。
假设驱动表的行数是 N,执行过程就要扫描驱动表 N 行,然后对于每一行,到被驱动表上匹配一次。
因此整个执行过程,近似复杂度是 N + N2log2M。
显然,N 对扫描行数的影响更大,因此应该让小表来做驱动表。
3.2 Simple Nested-Loop Join
现在,我们把 SQL 语句改成这样:
1
select * from t1 join t2 on (t1.a=t2.b);由于表 t2 的字段 b 上没有索引,因此再用图 2 的执行流程时,每次到 t2 去匹配的时候,就要做一次全表扫描。
你可以先设想一下这个问题,继续使用图 2 的算法,是不是可以得到正确的结果呢?如果只看结果的话,这个算法是正确的,而且这个算法也有一个名字,叫做“Simple Nested-Loop Join”。
但是,这样算来,这个 SQL 请求就要扫描表 t2 多达 100 次,总共扫描 100*1000=10 万行。
这还只是两个小表,如果 t1 和 t2 都是 10 万行的表(当然了,这也还是属于小表的范围),就要扫描 100 亿行,这个算法看上去太“笨重”了。
当然,MySQL 也没有使用这个 Simple Nested-Loop Join 算法,而是使用了另一个叫作“Block Nested-Loop Join”的算法,简称 BNL。
3.3 Block Nested-Loop Join
这时候,被驱动表上没有可用的索引,算法的流程是这样的:
- 把表 t1 的数据读入线程内存 join_buffer 中,由于我们这个语句中写的是 select *,因此是把整个表 t1 放入了内存;
- 扫描表 t2,把表 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条件的,作为结果集的一部分返回。
这个过程的流程图如下:

可以看到,在这个过程中,对表 t1 和 t2 都做了一次全表扫描,因此总的扫描行数是 1100。由于 join_buffer 是以无序数组的方式组织的,因此对表 t2 中的每一行,都要做 100 次判断,总共需要在内存中做的判断次数是:100*1000=10 万次。
前面我们说过,如果使用 Simple Nested-Loop Join 算法进行查询,扫描行数也是 10 万行。因此,从时间复杂度上来说,这两个算法是一样的。但是,Block Nested-Loop Join 算法的这 10 万次判断是内存操作,速度上会快很多,性能也更好.
假如t1很大以至于join_buffer放不下,那就是分段进行。
执行过程就变成了:
- 扫描表 t1,顺序读取数据行放入 join_buffer 中,放完第 88 行 join_buffer 满了,继续第 2 步;
- 扫描表 t2,把 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条件的,作为结果集的一部分返回;
- 清空 join_buffer;
- 继续扫描表 t1,顺序读取最后的 12 行数据放入 join_buffer 中,继续执行第 2 步。
3.4 Multi-Range Read 优化
接下来的几节都采用这个表结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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<=1000)do
insert into t1 values(i, 1001-i, i);
set i=i+1;
end while;
set i=1;
while(i<=1000000)do
insert into t2 values(i, i, i);
set i=i+1;
end while;
end;;
delimiter ;
call idata();对于这个语句
1
select * from t1 where a>=1 and a<=100;通过这个连接条件查到的id不一定是有序的

id 的值就变成随机的,那么就会出现随机访问,性能相对较差。虽然“按行查”这个机制不能改,但是调整查询的顺序,还是能够加速的。
因为大多数的数据都是按照主键递增顺序插入得到的,所以我们可以认为,如果按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能。
这,就是 MRR 优化的设计思路。此时,语句的执行流程变成了这样:
- 根据索引 a,定位到满足条件的记录,将 id 值放入 read_rnd_buffer 中 ;
- 将 read_rnd_buffer 中的 id 进行递增排序;
- 排序后的 id 数组,依次到主键 id 索引中查记录,并作为结果返回。
这里,read_rnd_buffer 的大小是由 read_rnd_buffer_size 参数控制的。如果步骤 1 中,read_rnd_buffer 放满了,就会先执行完步骤 2 和 3,然后清空 read_rnd_buffer。之后继续找索引 a 的下个记录,并继续循环。
另外需要说明的是,如果你想要稳定地使用 MRR 优化的话,需要设置set optimizer_switch="mrr_cost_based=off"。(官方文档的说法,是现在的优化器策略,判断消耗的时候,会更倾向于不使用 MRR,把 mrr_cost_based 设置为 off,就是固定使用 MRR 了。)
MRR 能够提升性能的核心在于,这条查询语句在索引 a 上做的是一个范围查询(也就是说,这是一个多值查询),可以得到足够多的主键 id。这样通过排序以后,再去主键索引查数据,才能体现出“顺序性”的优势。
3.5 Batched Key Access
理解了 MRR 性能提升的原理,我们就能理解 MySQL 在 5.6 版本后开始引入的 Batched Key Access(BKA) 算法了。这个 BKA 算法,其实就是对 NLJ 算法的优化。
NLJ 算法执行的逻辑是:从驱动表 t1,一行行地取出 a 的值,再到被驱动表 t2 去做 join。也就是说,对于表 t2 来说,每次都是匹配一个值。这时,MRR 的优势就用不上了。
那怎么才能一次性地多传些值给表 t2 呢?方法就是,从表 t1 里一次性地多拿些行出来,一起传给表 t2。
既然如此,我们就把表 t1 的数据取出来一部分,先放到一个临时内存。这个临时内存不是别人,就是 join_buffer。
通过上一篇文章,我们知道 join_buffer 在 BNL 算法里的作用,是暂存驱动表的数据。但是在 NLJ 算法里并没有用。那么,我们刚好就可以复用 join_buffer 到 BKA 算法中。

3.6 BNL 转 BKA
一些情况下,我们可以直接在被驱动表上建索引,这时就可以直接转成 BKA 算法了。但是,有时候你确实会碰到一些不适合在被驱动表上建索引的情况。
这时候,我们可以考虑使用临时表。使用临时表的大致思路是:
- 把表 t2 中满足条件的数据放在临时表 tmp_t 中;
- 为了让 join 使用 BKA 算法,给临时表 tmp_t 的字段 b 加上索引;
- 让表 t1 和 tmp_t 做 join 操作。
1
2
3
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>=1 and b<=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);


