MySQL学习点滴 --分区表
写在前面:笔者之前也有一些mysql方面的笔记,其中部分内容来自极客时间中丁奇老师的课程。后经园友提醒,这个做法确实不太好。之后我仍会继续更新一下mysql方面的学习记录,在自己理解之后用自己的方式记录下来。学习与记录,也是我写博客的初衷。
概述:
分区功能并不是在存储引擎层完成的,因此很多存储引擎包括innodb, myisam, ndb等都支持分区功能。但也并不是所有的存储引擎都支持分区。在使用分区前,首先要了解一下存储引擎对分区的支持情况。如果不作特殊说明,默认是在innodb下进行说明。
所谓分区,指的是将一个表或索引分解为更小的部分。从物理层面来说,可能是分成了n个物理分区,每个分区都是独立的。从逻辑上来说,这n个物理分区仍是一个表或一个索引。
分区可以分为两大类:
- 水平分区,指的是将同一表中不同行的记录分配到不同的物理文件中。
- 垂直分区,指的是将同一表中不同列的记录分配到不同的物理文件中。
mysql在5.1版本中加入了对水平分区的支持,其最新版本是否支持垂直分区笔者暂未考证。网上有一些关于mysql垂直分区的内容,大都也是在业务层面将表进行拆分,“手动的”垂直分区。
可以通过命令 show plugins; 来查看是否开启了分区功能。(partition的status值为active)。
mysql支持下面几种类型的分区:
- range分区:行数据基于一个给定连续区间的列值被放入分区。从5.5版本开始支持range columns的分区。
- list分区:和range分区类似,只是list分区面向的是离散的值。从5.5版本开始支持list columns的分区。
- hash分区:根据用户自定义的表达式的返回值进行分区,返回值不能为负数。
- key分区:根据mysql数据库提供的哈希函数来进行分区。
不论创建哪种类型的分区,如果表中存在主键或唯一索引,分区列必须是唯一索引的一个组成部分。(这里笔者初次接触时还闹了个笑话。上面提到了mysql支持的是水平分区,也就是说把不同的行记录分配到不同的物理文件中,那为啥还有个分区列?分区列还必须是唯一索引的一个组成部分?其实是这样的,在分区表中如果要插入一条记录,肯定要先确定应该插入到哪个分区里去。而确定它属于哪个分区就是依靠了分区列的值,根据这个值再按照不同分区自身的规则,就可以确定这条记录应该被分配到哪个分区了)。唯一索引可以是允许null值的,并且分区列只要是唯一索引的一个组成部分,不需要整个唯一索引分区列都是分区列。如唯一索引是 unique key(a,b),分区列可以只指定a列,partition by hash(a);
如果创建表时没有指定主键,唯一索引,那么可以指定任何一个列为分区列。
分区类型:
range分区:
这是最常用的一种分区,我们来看一个简单的例子:
create table range_t(
id int
)engine = innodb
partition by range (id)(
partition p0 values less than (10),
partition p1 values less than (20)
);
由建表语句很容易看出我们把range_t分成了p0,p1两部分。由于range分区是给连续区间分区,因此p1的区间范围其实[10,20)。此处需要注意的是,如果仅仅按照上面我们的分区来的话,是不能向表range_t中插入id大于20的记录的。
这种情况下我们可以添加一个maxvalue值的分区,maxvalue可以理解为正无穷,因此区间可以改变为[20,maxvalue).
alter table range_t add partition(
partition p2 values less than maxvalue);
range分区的一个典型的应用场景是记录与日期相关的记录。例如要记录某种交易记录,可以按年份时间进行分区。如
...
partition by range (year(date))(
partition p2017 values less than (2018),
partition p2018 values less than (2019),
partition p2019 values less than (2020)
);
根据date所属的年份进行分区,year函数取得的值如果小于2019就归档到p2018分区。这样做有这么一些好处。首先是方便管理,如果我们要删除18年的数据,可以直接对分区p2018进行删除。 alter table t drop partition p2018; 另一方面,这样分区也可以加快某些查询的速度。同样以p2018分区进行举例,如果你确定要查询的范围只在2018这个区间内,可以直接对这个分区进行查询:select * from t partition(p2018);
有一点需要注意,优化器可以对range分区中的部分函数(如year(),to_days()...)进行优化选择,而对形如year(date)*100 + month(date)这样的分区条件是无能为力的。
list分区:
list分区和range分区很相似,区别在于list分区的值是离散的。例如建表语句:
create table list_t(
a int,
b int
)engine = innodb
partition by list(b)(
partition p0 values in(1,3,5,7),
partition p1 values in(2,4,6,8)
);
和range分区一样,如果你插入的记录的分区列的值不在list分区的范围内,mysql数据库会抛出异常。另外,如果一次插入多个行的记录,而这些记录当中存在分区未定义的值时,myisam和innodb存储引擎的处理方式不同。myisam会将之前的行数据库都插入,但之后的不会插入。而innodb会将其视为一个事务,因此没有任何事务插入。
myisam:
innodb:
hash分区:
hash分区的目的是将数据均匀地分部到预先定义的各个分区中,尽量保证各分区的数据量相等。在range和list分区中,必须明确指定一个给定的列值活列值所在的集合范围,而hash分区中,mysql自动完成这些工作,用户所要做的只是基于将要进行哈希分区的列指定一个列值或表达式,以及指定被分区的表将要被分隔成几部分。一个hash分区的建表语句例子如下:
create table hash_t(
a int,
b int
)engine = innodb
partition by hash(a+b)
partitions 4;
如上所述,用户所要做的只是基于将要进行哈希分区的列指定一个列值或表达式。这里我们使用a+b的值来作为进行hash的值,当然你也可以直接使用字段a或b,或是别的表达式。另外,后面的partitions 4;代表了要分隔成几个区,这里要求是一个非负整数,默认值是1.
key分区:
key分区和hash分区很类似,区别在于hash分区使用用户定义的函数进行分区,key分区使用mysql数据库提供的函数进行分区。对于ndb cluster引擎,mysql使用md5函数来进行分区,对于其他引擎,mysql数据库使用其内部哈希函数,这些函数基于与password()一样的运算规则。
columns分区:
前面介绍的这几种分区有一个共同条件,即数据必须是整型(interger),如果不是整形则需要通过函数将其转化为整型,如year()等。从5.5版本开始,mysql支持columns分区,可以理解成是range分区和list分区的一种优化,它允许直接使用非整形的数据进行分区,分区根据类型直接比较而得,不需要额外的转型处理。此外,range columns允许对多个列的值进行分区。columns分区所支持的类型:
- 所有的整型类型,如int,smallint,tinyint,bigint。注意,float和decimal不支持。
- 日期类型,仅支持date和datetime。
- 字符串类型,如char,varchar,binaryhe varbinary。注意,blob和text不支持。
range columns对多个列的值进行分区的例子如下:
create table range_column_t(
a int,
b int,
c char(3)
)engine = innodb
partition by range columns(a,b,c)(
partition p0 values less than (5,10,'c'),
partition p1 values less than (10,20,'m'),
partition p2 values less than (30,50,'z')
);
到这里,你应该和我一样有一个疑问,如果我三个值分别属于不同的区间则会被插入到哪个分区呢。比如插入这么一条记录:insert into range_column_t values(4,9,'n'); a,b字段的值都在p0分区范围内,c的值p2分区范围内,实际上也插入成功了。我们来看看结果吧:
如果我们再插入一条记录:insert into range_column_t values(25,15,'a');查看结果如下:
看来这种方式下,是按照分区列的顺序进行分区的,满足第一个条件后就会直接被分配到对应分区。
子分区:
子分区是在分区的基础上再进行分区,有时也称这种分区为复合分区。mysql数据库允许在range和list的分区上再进行hash分区或key的子分区。一个建立子分区的例子:
create table sub_t(
a int,
b date
)engine = innodb
partition by range(year(b))
subpartition by hash(to_days(b))
subpartitions 2 (
partition p0 values less than (1990),
partition p1 values less than (2000),
partition p2 values less than maxvalue
);
关于子分区,有几个地方需要注意一下:
- 每个子分区的数量必须相同。
- 要在一个分区表的任何分区上使用subpartition来明确定义任何子分区,就必须定义所有的子分区。
- 每个subpartition子句必须包括子分区的一个名字
- 子分区的名字必须是唯一的。
null值:
mysql数据库允许对null值做分区,但处理方式可能不同于其他数据库。在mysql的分区中,null值被认为总是小于任何一个非null值。并且对于不同的分区类型,处理方式也稍有不同。
- 对于range分区,null值会被放入最左边的分区。因此,如果删除最左侧的分区,假设该分区是定义是 less than 10,那么删除的实际上是小于10的所有记录和包含null值的记录。
- 在list分区中则必须显示的指出哪个分区中放入null值,否则会报错。
- 而在hash和key分区中,任何分区函数都会将含有null值的记录返回为0。
分区和性能:
其实有些类似于索引,并不是说无脑地添加索引,或是使用分区,数据库的查询就会更快。我们真正要做的是根据实际业务需求去具体的看待问题,如前面提到的按时间记录的交易记录的表,假设有这样的一张大表,并且可以明确的按照时间分区,且需要频繁访问。那么确实是可以使用分区来提高效率,每次查询时尽量只访问对应的分区即可。
但实际情况中也可能存在这么一种类型的表,它的数据量也很大,访问也很频繁。但每次可能只是会通过索引去访问几条记录,而不需要一次返回很多很多记录。这种情况下,分区可能会带来不好的影响。我们知道,正常情况下b+树索引(mysql索引采用b+树结构)只需要2~3次io操作即可找到对应的记录。如果盲目地使用分区,反而可能会增加io操作的次数。