一、本节概述

我经常被问到这样一个问题:分区表有什么问题,为什么公司规范不让使用分区表呢?今
天,我们就来聊聊分区表的使用行为,然后再一起回答这个问题。

二、分区表是什么?

为了说明分区表的组织形式,我先创建一个表 t:

图 1 表 t 的磁盘文件

我在表 t 中初始化插入了两行记录,按照定义的分区规则,这两行记录分别落在 p_2018
和 p_2019 这两个分区上。

可以看到,这个表包含了一个.frm 文件和 4 个.ibd 文件,每个分区对应一个.ibd 文件。也
就是说:

对于引擎层来说,这是 4 个表;
对于 Server 层来说,这是 1 个表。

你可能会觉得这两句都是废话。其实不然,这两句话非常重要,可以帮我们理解分区表的
执行逻辑。

三、分区表的引擎层行为

1、分区表加间隙锁的例子

我先给你举个在分区表加间隙锁的例子,目的是说明对于 InnoDB 来说,这是 4 个表。

图 2 分区表间隙锁示例

这里顺便复习一下,我在第 21 篇文章和你介绍的间隙锁加锁规则。

我们初始化表 t 的时候,只插入了两行数据, ftime 的值分别是,‘2017-4-1’
和’2018-4-1’ 。session A 的 select 语句对索引 ftime 上这两个记录之间的间隙加了
锁。如果是一个普通表的话,那么 T1 时刻,在表 t 的 ftime 索引上,间隙和加锁状态应
该是图 3 这样的。

图 3 普通表的加锁范围

也就是说,‘2017-4-1’ 和’2018-4-1’ 这两个记录之间的间隙是会被锁住的。那么,
sesion B 的两条插入语句应该都要进入锁等待状态。

但是,从上面的实验效果可以看出,session B 的第一个 insert 语句是可以执行成功的。
这是因为,对于引擎来说,p_2018 和 p_2019 是两个不同的表,也就是说 2017-4-1 的
下一个记录并不是 2018-4-1,而是 p_2018 分区的 supremum。所以 T1 时刻,在表 t
的 ftime 索引上,间隙和加锁的状态其实是图 4 这样的:

图 4 分区表 t 的加锁范围

由于分区表的规则,session A 的 select 语句其实只操作了分区 p_2018,因此加锁范围
就是图 4 中深绿色的部分。

所以,session B 要写入一行 ftime 是 2018-2-1 的时候是可以成功的,而要写入 2017-
12-1 这个记录,就要等 session A 的间隙锁。

图 5 就是这时候的 show engine innodb status 的部分结果。

图 5 session B 被锁住信息

看完 InnoDB 引擎的例子,我们再来一个 MyISAM 分区表的例子。
我首先用 alter table t engine=myisam,把表 t 改成 MyISAM 表;然后,我再用下面这
个例子说明,对于 MyISAM 引擎来说,这是 4 个表。

图 6 用 MyISAM 表锁验证

在 session A 里面,我用 sleep(100) 将这条语句的执行时间设置为 100 秒。由于
MyISAM 引擎只支持表锁,所以这条 update 语句会锁住整个表 t 上的读。
但我们看到的结果是,session B 的第一条查询语句是可以正常执行的,第二条语句才进
入锁等待状态。

这正是因为 MyISAM 的表锁是在引擎层实现的,session A 加的表锁,其实是锁在分区
p_2018 上。因此,只会堵住在这个分区上执行的查询,落到其他分区的查询是不受影响
的。

看到这里,你可能会说,分区表看来还不错嘛,为什么不让用呢?我们使用分区表的一个
重要原因就是单表过大。那么,如果不使用分区表的话,我们就是要使用手动分表的方
式。

3、接下来,我们一起看看手动分表和分区表有什么区别。

比如,按照年份来划分,我们就分别创建普通表 t_2017、t_2018、t_2019 等等。手工分
表的逻辑,也是找到需要更新的所有分表,然后依次执行更新。在性能上,这和分区表并
没有实质的差别。

分区表和手工分表,一个是由 server 层来决定使用哪个分区,一个是由应用层代码来决定
使用哪个分表。因此,从引擎层看,这两种方式也是没有差别的。

其实这两个方案的区别,主要是在 server 层上。从 server 层看,我们就不得不提到分区
表一个被广为诟病的问题:打开表的行为。

四、分区策略

每当第一次访问一个分区表的时候,MySQL 需要把所有的分区都访问一遍。一个典型的
报错情况是这样的:如果一个分区表的分区很多,比如超过了 1000 个,而 MySQL 启动
的时候,open_files_limit 参数使用的是默认值 1024,那么就会在访问这个表的时候,由
于需要打开所有的文件,导致打开表文件的个数超过了上限而报错。

下图就是我创建的一个包含了很多分区的表 t_myisam,执行一条插入语句后报错的情
况。

图 7 insert 语句报错

可以看到,这条 insert 语句,明显只需要访问一个分区,但语句却无法执行。
这时,你一定从表名猜到了,这个表我用的是 MyISAM 引擎。是的,因为使用 InnoDB
引擎的话,并不会出现这个问题。

MyISAM 分区表使用的分区策略,我们称为通用分区策略(generic partitioning),每
次访问分区都由 server 层控制。通用分区策略,是 MySQL 一开始支持分区表的时候就存
在的代码,在文件管理、表管理的实现上很粗糙,因此有比较严重的性能问题。
从 MySQL 5.7.9 开始,InnoDB 引擎引入了本地分区策略(native partitioning)。这个
策略是在 InnoDB 内部自己管理打开分区的行为。

MySQL 从 5.7.17 开始,将 MyISAM 分区表标记为即将弃用 (deprecated),意思是“从
这个版本开始不建议这么使用,请使用替代方案。在将来的版本中会废弃这个功能”。

从 MySQL 8.0 版本开始,就不允许创建 MyISAM 分区表了,只允许创建已经实现了本地
分区策略的引擎。目前来看,只有 InnoDB 和 NDB 这两个引擎支持了本地分区策略。
接下来,我们再看一下分区表在 server 层的行为。

五、分区表的 server 层行为

如果从 server 层看的话,一个分区表就只是一个表。

这句话是什么意思呢?接下来,我就用下面这个例子来和你说明。如图 8 和图 9 所示,分
别是这个例子的操作序列和执行结果图。

图 8 分区表的 MDL 锁

图 9 show processlist 结果

可以看到,虽然 session B 只需要操作 p_2107 这个分区,但是由于 session A 持有整个
表 t 的 MDL 锁,就导致了 session B 的 alter 语句被堵住。

这也是 DBA 同学经常说的,分区表,在做 DDL 的时候,影响会更大。如果你使用的是普
通分表,那么当你在 truncate 一个分表的时候,肯定不会跟另外一个分表上的查询语句,
出现 MDL 锁冲突。

到这里我们小结一下:

1. MySQL 在第一次打开分区表的时候,需要访问所有的分区;
2. 在 server 层,认为这是同一张表,因此所有分区共用同一个 MDL 锁;
3. 在引擎层,认为这是不同的表,因此 MDL 锁之后的执行过程,会根据分区表规则,只
访问必要的分区。

而关于“必要的分区”的判断,就是根据 SQL 语句中的 where 条件,结合分区规则来实
现的。比如我们上面的例子中,where ftime=‘2018-4-1’,根据分区规则 year 函数算
出来的值是 2018,那么就会落在 p_2019 这个分区。

但是,如果这个 where 条件改成 where ftime>=‘2018-4-1’,虽然查询结果相同,但
是这时候根据 where 条件,就要访问 p_2019 和 p_others 这两个分区。

如果查询语句的 where 条件中没有分区 key,那就只能访问所有分区了。当然,这并不是
分区表的问题。即使是使用业务分表的方式,where 条件中没有使用分表的 key,也必须
访问所有的分表。

我们已经理解了分区表的概念,那么什么场景下适合使用分区表呢?

六、分区表的应用场景

分区表的一个显而易见的优势是对业务透明,相对于用户分表来说,使用分区表的业务代
码更简洁。还有,分区表可以很方便的清理历史数据。

如果一项业务跑的时间足够长,往往就会有根据时间删除历史数据的需求。这时候,按照
时间分区的分区表,就可以直接通过 alter table t drop partition …这个语法删掉分区,
从而删掉过期的历史数据。

这个 alter table t drop partition …操作是直接删除分区文件,效果跟 drop 普通表类
似。与使用 delete 语句删除数据相比,优势是速度快、对系统影响小。

七、小结

这篇文章,我主要和你介绍的是 server 层和引擎层对分区表的处理方式。我希望通过这些
介绍,你能够对是否选择使用分区表,有更清晰的想法。

需要注意的是,我是以范围分区(range)为例和你介绍的。实际上,MySQL 还支持
hash 分区、list 分区等分区方法。你可以在需要用到的时候,再翻翻手册。

实际使用时,分区表跟用户分表比起来,有两个绕不开的问题:一个是第一次访问的时候
需要访问所有分区,另一个是共用 MDL 锁。

因此,如果要使用分区表,就不要创建太多的分区。我见过一个用户做了按天分区策略,
然后预先创建了 10 年的分区。这种情况下,访问分区表的性能自然是不好的。这里有两
个问题需要注意:

1. 分区并不是越细越好。实际上,单表或者单分区的数据一千万行,只要没有特别大的索
引,对于现在的硬件能力来说都已经是小表了。
2. 分区也不要提前预留太多,在使用之前预先创建即可。比如,如果是按月分区,每年年
底时再把下一年度的 12 个新分区创建上即可。对于没有数据的历史分区,要及时的
drop 掉。

至于分区表的其他问题,比如查询需要跨多个分区取数据,查询性能就会比较慢,基本上
就不是分区表本身的问题,而是数据量的问题或者说是使用方式的问题了。

当然,如果你的团队已经维护了成熟的分库分表中间件,用业务分表,对业务开发同学没
有额外的复杂性,对 DBA 也更直观,自然是更好的。

最后,我给你留下一个思考题吧。

我们举例的表中没有用到自增主键,假设现在要创建一个自增字段 id。MySQL 要求分区
表中的主键必须包含分区字段。如果要在表 t 的基础上做修改,你会怎么定义这个表的主
键呢?为什么这么定义呢?

你可以把你的结论和分析写在留言区,我会在下一篇文章的末尾和你讨论这个问题。感谢
你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。

八、上期问题时间

上篇文章后面还不够多,可能很多同学还没来记得看吧,我们就等后续有更多留言的时
候,再补充本期的“上期问题时间”吧。

@夹心面包 提到了在 grant 的时候是支持通配符的:"_"表示一个任意字符,“%”表示任
意字符串。这个技巧在一个分库分表方案里面,同一个分库上有多个 db 的时候,是挺方
便的。不过我个人认为,权限赋值的时候,控制的精确性还是要优先考虑的。

MySQL实战45讲学习笔记:第四十三讲的更多相关文章

  1. MySQL实战45讲学习笔记:第十三讲

    一.引子 经常会有同学来问我,我的数据库占用空间太大,我把一个最大的表删掉了一半的数据,怎么表文件的大小还是没变? 那么今天,我就和你聊聊数据库表的空间回收,看看如何解决这个问题. 这里,我们还是针对 ...

  2. MySQL实战45讲学习笔记:第二十三讲

    一.本节概要 今天这篇文章,我会继续和你介绍在业务高峰期临时提升性能的方法.从文章标题“MySQL 是怎么保证数据不丢的?”,你就可以看出来,今天我和你介绍的方法,跟数据的可靠性有关. 在专栏前面文章 ...

  3. MySQL实战45讲学习笔记:第二十四讲

    一.引子 在前面的文章中,我不止一次地和你提到了 binlog,大家知道 binlog 可以用来归档,也可以用来做主备同步,但它的内容是什么样的呢?为什么备库执行了 binlog 就可以跟主库保持一致 ...

  4. MySQL实战45讲学习笔记:第十五讲

    一.引子 在今天这篇答疑文章更新前,MySQL 实战这个专栏已经更新了 14 篇.在这些文章中,大家在评论区留下了很多高质量的留言.现在,每篇文章的评论区都有热心的同学帮忙总结文章知识点,也有不少同学 ...

  5. MySQL实战45讲学习笔记:第十四讲

    一.引子 在开发系统的时候,你可能经常需要计算一个表的行数,比如一个交易系统的所有变更记录总数.这时候你可能会想,一条 select count(*) from t 语句不就解决了吗? 但是,你会发现 ...

  6. MySQL实战45讲学习笔记:第二十五讲

    一.引子 在上一篇文章中,我和你介绍了 binlog 的基本内容,在一个主备关系中,每个备库接收主库的 binlog 并执行. 正常情况下,只要主库执行更新生成的所有 binlog,都可以传到备库并被 ...

  7. MySQL实战45讲学习笔记:第二十六讲

    一.引子 在上一篇文章中,我和你介绍了几种可能导致备库延迟的原因.你会发现,这些场景里,不论是偶发性的查询压力,还是备份,对备库延迟的影响一般是分钟级的,而且在备库恢复正常以后都能够追上来. 但是,如 ...

  8. MySQL实战45讲学习笔记:第十讲

    一 .本节内容概要 前面我们介绍过索引,你已经知道了在 MySQL 中一张表其实是可以支持多个索引的.但是,你写 SQL 语句的时候,并没有主动指定使用哪个索引.也就是说,使用哪个索引是由MySQL ...

  9. MySQL实战45讲学习笔记:第十二讲

    一.引子 平时的工作中,不知道你有没有遇到过这样的场景,一条 SQL 语句,正常执行的时候特别快,但是有时也不知道怎么回事,它就会变得特别慢,并且这样的场景很难复现,它不只随机,而且持续时间还很短. ...

  10. MySQL实战45讲学习笔记:第十六讲

    一.今日内容概要 在你开发应用的时候,一定会经常碰到需要根据指定的字段排序来显示结果的需求.还是以我们前面举例用过的市民表为例,假设你要查询城市是“杭州”的所有人名字,并且按照姓名排序返回前 1000 ...

随机推荐

  1. C语言验证哥德巴赫猜想

    #include<stdio.h>int f(int x);int main(void){    int n,i;  scanf("%d",&n);  for( ...

  2. java之获取变量的类型

    java要获取变量的类型必须自己定义一个函数: public class Test{ public static void main(String[] args) { short a = 1; a + ...

  3. Tensorflow.nn 核心模块详解

    看过前面的例子,会发现实现深度神经网络需要使用 tensorflow.nn 这个核心模块.我们通过源码来一探究竟. # Copyright 2015 Google Inc. All Rights Re ...

  4. 解决SVN 被锁且Cleanup无效问题

    开发两年多,依然用svn做代码管理工具,看到隔壁java组用git,心向往之,奈何苦苦不得机会,既然用svn,那么就说一说svn碰到的问题如何解决吧. 有时候我们在提交,或者更新代码时,由于网络或其他 ...

  5. Elasticsearch Query DSL 语言介绍

    目录 0. 引言 1. 组合查询 2. 全文搜索 2.1 Match 2.2 Match Phase 2.3 Multi Match 2.4 Query String 2.5 Simple Query ...

  6. cmdb全总结

    1.什么是cmdb ,做什么的? 配置管理数据库 ,就是存储基础设施的信息配置使用的 简单说就是CMDB这个系统可以自动发现网络上的IT设备 ,并自动存储相关信息 ,像一台服务器有型号 厂商 系统 c ...

  7. 程序员的自我修养系列(一):优雅的代码管理工具之GitHub

    1.导言 代码管理是程序员经常遇到一个问题,很多童鞋将代码保存到本地硬盘,此种方法管理混乱,也存在代码丢失的风险,且版本无法控制,因此养成良好的代码管理习惯是程序员的必修课.在众多代码管理工具中笔者在 ...

  8. 判断map是否包含另一个map

    判断map是否包含另一个map: map不同与list集合,list集合有直接判断集合是否包含其他集合或者元素的方法. boolean contains(Object o) 如果list包含指定的元素 ...

  9. ms sql事务输出错误

    begin try 语句 end trybegin catch --ERROR_NUMBER() 返回错误号. --ERROR_SEVERITY() 返回严重性. --ERROR_STATE() 返回 ...

  10. 前端基础之BOM和DOM操作

    目录 BOM和DOM定义 windows对象 windows的子对象 navigator对象 screen对象 history对象 location对象 弹出框 警告框 确认框 提示框 计时相关 se ...