gitbook/MySQL实战45讲/docs/82560.md

192 lines
12 KiB
Markdown
Raw Permalink Normal View History

2022-09-03 22:05:03 +08:00
# 43 | 要不要使用分区表?
我经常被问到这样一个问题:分区表有什么问题,为什么公司规范不让使用分区表呢?今天,我们就来聊聊分区表的使用行为,然后再一起回答这个问题。
# 分区表是什么?
为了说明分区表的组织形式我先创建一个表t
```
CREATE TABLE `t` (
`ftime` datetime NOT NULL,
`c` int(11) DEFAULT NULL,
KEY (`ftime`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
PARTITION BY RANGE (YEAR(ftime))
(PARTITION p_2017 VALUES LESS THAN (2017) ENGINE = InnoDB,
PARTITION p_2018 VALUES LESS THAN (2018) ENGINE = InnoDB,
PARTITION p_2019 VALUES LESS THAN (2019) ENGINE = InnoDB,
PARTITION p_others VALUES LESS THAN MAXVALUE ENGINE = InnoDB);
insert into t values('2017-4-1',1),('2018-4-1',1);
```
![](https://static001.geekbang.org/resource/image/06/f5/06f041129783533de9c75580f9decdf5.png)
图1 表t的磁盘文件
我在表t中初始化插入了两行记录按照定义的分区规则这两行记录分别落在p\_2018和p\_2019这两个分区上。
可以看到,这个表包含了一个.frm文件和4个.ibd文件每个分区对应一个.ibd文件。也就是说
* 对于引擎层来说这是4个表
* 对于Server层来说这是1个表。
你可能会觉得这两句都是废话。其实不然,这两句话非常重要,可以帮我们理解分区表的执行逻辑。
# 分区表的引擎层行为
我先给你举个在分区表加间隙锁的例子目的是说明对于InnoDB来说这是4个表。
![](https://static001.geekbang.org/resource/image/d2/c7/d28d6ab873bd8337d88812d45b9266c7.png)
图2 分区表间隙锁示例
这里顺便复习一下,我在[第21篇文章](https://time.geekbang.org/column/article/75659)和你介绍的间隙锁加锁规则。
我们初始化表t的时候只插入了两行数据 ftime的值分别是'2017-4-1' 和'2018-4-1' 。session A的select语句对索引ftime上这两个记录之间的间隙加了锁。如果是一个普通表的话那么T1时刻在表t的ftime索引上间隙和加锁状态应该是图3这样的。
![](https://static001.geekbang.org/resource/image/27/d2/273c9ca869f5b52621641d73eb6f72d2.jpg)
图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这样的
![](https://static001.geekbang.org/resource/image/92/5c/92f63aba0b24adefac7316c75463b95c.jpg)
图4 分区表t的加锁范围
由于分区表的规则session A的select语句其实只操作了分区p\_2018因此加锁范围就是图4中深绿色的部分。
所以session B要写入一行ftime是2018-2-1的时候是可以成功的而要写入2017-12-1这个记录就要等session A的间隙锁。
图5就是这时候的show engine innodb status的部分结果。
![](https://static001.geekbang.org/resource/image/e3/0f/e3d83d9ba89de9a6f541c9a2f24a3b0f.png)
图5 session B被锁住信息
看完InnoDB引擎的例子我们再来一个MyISAM分区表的例子。
我首先用alter table t engine=myisam把表t改成MyISAM表然后我再用下面这个例子说明对于MyISAM引擎来说这是4个表。
![](https://static001.geekbang.org/resource/image/94/76/941306d4a7193455dcf1cfebf7678876.png)
图6 用MyISAM表锁验证
在session A里面我用sleep(100)将这条语句的执行时间设置为100秒。由于MyISAM引擎只支持表锁所以这条update语句会锁住整个表t上的读。
但我们看到的结果是session B的第一条查询语句是可以正常执行的第二条语句才进入锁等待状态。
这正是因为MyISAM的表锁是在引擎层实现的session A加的表锁其实是锁在分区p\_2018上。因此只会堵住在这个分区上执行的查询落到其他分区的查询是不受影响的。
看到这里,你可能会说,分区表看来还不错嘛,为什么不让用呢?我们使用分区表的一个重要原因就是单表过大。那么,如果不使用分区表的话,我们就是要使用手动分表的方式。
接下来,我们一起看看手动分表和分区表有什么区别。
比如按照年份来划分我们就分别创建普通表t\_2017、t\_2018、t\_2019等等。手工分表的逻辑也是找到需要更新的所有分表然后依次执行更新。在性能上这和分区表并没有实质的差别。
分区表和手工分表一个是由server层来决定使用哪个分区一个是由应用层代码来决定使用哪个分表。因此从引擎层看这两种方式也是没有差别的。
其实这两个方案的区别主要是在server层上。从server层看我们就不得不提到分区表一个被广为诟病的问题打开表的行为。
# 分区策略
每当第一次访问一个分区表的时候MySQL需要把所有的分区都访问一遍。**一个典型的报错情况**是这样的如果一个分区表的分区很多比如超过了1000个而MySQL启动的时候open\_files\_limit参数使用的是默认值1024那么就会在访问这个表的时候由于需要打开所有的文件导致打开表文件的个数超过了上限而报错。
下图就是我创建的一个包含了很多分区的表t\_myisam执行一条插入语句后报错的情况。
![](https://static001.geekbang.org/resource/image/ab/e7/abfa0054ec43d97fb18ba3c1c8829ae7.png)
图 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所示分别是这个例子的操作序列和执行结果图。
![](https://static001.geekbang.org/resource/image/0e/81/0eca5a3190161e59ea58493915bd5e81.png)
图8 分区表的MDL锁
![](https://static001.geekbang.org/resource/image/af/a8/afe662f5e051a2ceb96a87624a589aa8.png)
图9 show processlist结果
可以看到虽然session B只需要操作p\_2017这个分区但是由于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分区等分区方法。你可以在需要用到的时候再翻翻[手册](https://dev.mysql.com/doc/refman/8.0/en/partitioning-types.html)。
实际使用时分区表跟用户分表比起来有两个绕不开的问题一个是第一次访问的时候需要访问所有分区另一个是共用MDL锁。
因此如果要使用分区表就不要创建太多的分区。我见过一个用户做了按天分区策略然后预先创建了10年的分区。这种情况下访问分区表的性能自然是不好的。这里有两个问题需要注意
1. 分区并不是越细越好。实际上,单表或者单分区的数据一千万行,只要没有特别大的索引,对于现在的硬件能力来说都已经是小表了。
2. 分区也不要提前预留太多在使用之前预先创建即可。比如如果是按月分区每年年底时再把下一年度的12个新分区创建上即可。对于没有数据的历史分区要及时的drop掉。
至于分区表的其他问题,比如查询需要跨多个分区取数据,查询性能就会比较慢,基本上就不是分区表本身的问题,而是数据量的问题或者说是使用方式的问题了。
当然如果你的团队已经维护了成熟的分库分表中间件用业务分表对业务开发同学没有额外的复杂性对DBA也更直观自然是更好的。
最后,我给你留下一个思考题吧。
我们举例的表中没有用到自增主键假设现在要创建一个自增字段id。MySQL要求分区表中的主键必须包含分区字段。如果要在表t的基础上做修改你会怎么定义这个表的主键呢为什么这么定义呢
你可以把你的结论和分析写在留言区,我会在下一篇文章的末尾和你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。
# 上期问题时间
上篇文章后面还不够多,可能很多同学还没来记得看吧,我们就等后续有更多留言的时候,再补充本期的“上期问题时间”吧。
@夹心面包 提到了在grant的时候是支持通配符的"\_"表示一个任意字符,“%”表示任意字符串。这个技巧在一个分库分表方案里面同一个分库上有多个db的时候是挺方便的。不过我个人认为权限赋值的时候控制的精确性还是要优先考虑的。