一、子分区
- 子分区是在分区的基础上再进行分区,有时也称这种分区为复合分区
- MySQL数据库允许在RANGE和LIST的分区上再进行HASH或KEY的子分区
演示案例
- 创建下面一个表,对b字段的年份进行RANGE分区,然后再根据b字段的月份进行分区HASH分区:
- 分区之后分区的数量为(3*2=6)个,因此物理磁盘上的文件就会变为6个.ibd文件
- 另外,此处代表的意思是3个range分区,然后在每个range分区再被分为2个hash分区
- 其中p0、p1、p2代表3个range分区的名字,hash子分区的名字取系统默认值
create table ts( 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”语法来显式地指定各个子分区的名字,使用之后,hash子分区的“subpartitions 2”就可以省略了
create table ts2( a int,b date )engine=innodb partition by range(year(b)) subpartition by hash(to_days(b))( partition p0 values less than (1990)( subpartition s0, subpartition s1 ), partition p1 values less than (2000)( subpartition s2, subpartition s3 ), partition p2 values less than MAXVALUE( subpartition s4, subpartition s5 ) );
建立子分区的注意事项
- ①每个子分区的数量必须相同
- ②要在一个分区表的任何分区上使用subpartition来明确定义任何子分区,就必须定义所有的子分区。因此下面的语句是错误的,下面的range分区的p1分区没有明确子分区
create table ts2( a int,b date )engine=innodb partition by range(year(b)) subpartition by hash(to_days(b))( partition p0 values less than (1990)( subpartition s0, subpartition s1 ), partition p1 values less than (2000), partition p2 values less than MAXVALUE( subpartition s4, subpartition s5 ) );
- ③每个subpartition子句必须包括子分区的一个名字
- ④子分区的名字必须是唯一的。因此下面的创建语句是错误的
create table ts2( a int,b date )engine=innodb partition by range(year(b)) subpartition by hash(to_days(b))( partition p0 values less than (1990)( subpartition s0, subpartition s1 ), partition p1 values less than (2000)( subpartition s0, subpartition s1 ), partition p2 values less than MAXVALUE( subpartition s0, subpartition s1 ) );
二、分区中的NULL值
- MySQL数据库允许对NULL值做分区,但是处理的方法和其他数据库可能完全不同。MySQL数据库的分区总是视NULL值小于任何的一个非NULL值,这和MySQL中处理NULL值的order by操作是一样的
- 并且MySQL数据库对于不同的分区类型,对于NULL值的处理也不相同
RANGE分区中NULL值的处理
- 对于RANGE分区,如果向分区列插入了NULL值,则数据会被放置在最左边的分区
- 例如,对下面的表分了3个区
create table t_range( a int, b int )engine=innodb partition by range(b)( partition p0 values less than (10), partition p1 values less than (20), partition p2 values less than maxvalue );
- 下面我们向数据库中插入一条记录,并且分区字段的值为NULL值,那么NULL值会被插入到p0分区中:
insert into t_range select 1,NULL;
- 查看分区的情况,与预期的一致:
select table_name,partition_name,table_rows from information_schema.partitions where table_schema=database() and table_name='t_range';
- 注意:因为NULL被放入了最左边的分区,因此如果删除p0分区,所有的NULL值字段也被删除了
- 例如,下面删除分区,然后再次查看
alter table t_range drop partition p0; select * from t_range; select table_name,partition_name,table_rows from information_schema.partitions where table_schema=database() and table_name='t_range';
LIST分区中NULL值的处理
- 对于LIST分区,必须显式的指定NULL值被放入哪个分区,如果不指定会报错
- 例如,下面创建一个表,使用LIST分区,但是没有指定NULL值被放入哪个分区中,因此插入数据报错
create table t_list( a int, b int )engine=innodb partition by list(b)( partition p0 values in (1,3,5,7,9), partition p1 values in (2,4,6,8,10) ); insert into t_list select 1,NULL;
- 现在我们显式地将NULL值放入p0分区中,然后再进行插入,显式成功
drop table if exists t_list; create table t_list( a int, b int )engine=innodb partition by list(b)( partition p0 values in (1,3,5,7,9,NULL), partition p1 values in (2,4,6,8,10) ); insert into t_list select 1,NULL; select table_name,partition_name,table_rows from information_schema.partitions where table_schema=database() and table_name='t_list';
HASH、KEY分区中NULL值的处理
- HASH和KEY分区对于NULL值的处理,都认为NULL值为0,因此会将NULL作为0来处理
- 下面创建一个表,使用HASH分区,分4个区
create table t_hash( a int, b int )engine=innodb partition by hash(b) partitions 4;
- 插入两行数据
insert into t_hash select 1,0; insert into t_hash select 1,NULL;
- 查看分区情况,与预期的一直,NULL被作为数字0处理,插入在了分区1中
select table_name,partition_name,table_rows from information_schema.partitions where table_schema=database() and table_name='t_hash';
三、分区和性能
- 对表做分区不一定会提升数据库的性能,如果使用不合理反而会降低性能
OLTP、OLAP应用
- 数据库的应用分为两类:
- 一类是OLTP(在线事务处理),如Blog、电子商务、网络游戏等
- 另一类是OLAP(在线分析处理),如数据仓库、数据集市
- 在一个实际的应用环境中,可能既有OLTP的应用,也有OLAP的应用。如网络游戏中,玩家操作的游戏数据库应用就是OLTP的,但是游戏厂商可能需要对游戏产生的日志进行分析,通过分析得到的结果来更好地服务于游戏,预测玩家的行为等,而这却是OLAP的应用
- 对于OLAP的应用,分区的确是可以很好地提高查询的性能,因为OLAP应用大多数查询需要频繁地扫描一张很大的表。假设有一张1亿行的表,其中有一个时间戳属性列。用户的查询需要从这张表中获取一年的数据。如果按时间戳进行分区,则只需要扫描相应的分区即可
- 然而对于OLTP的应用,分区应该非常小心。在这种应用下,通常不可能会获取张大表中10%的数据,大部分都是通过索引返回几条记录即可。而根据B+树索引的原理可知,对于一张大表,一般的B+树需要2~3次的磁盘IO。因此B+树可以很好地完成操作,不需要分区的帮助,并且设计不好的分区会带来严重的性能问题
演示说明
- 我发现很多开发团队会认为含有1000W行的表是一张非常巨大的表,所以他们往往会选择采用分区,如对主键做10个HASH的分区,这样每个分区就只有100W的数据了,因此查询应该变得更快了,如 SELECT* FROM TABLE WHERE PK=@pk
- 但是有没有考虑过这样一种情况:100W和1000行的数据本身构成的B+树的层次都是一样的,可能都是2层。那么上述走主键分区的索引并不会带来性能的提高。好的,如果1000W的B+树的高度是3,100W的B+树的高度是2,那么上述按主键分区的索引可以避免1次IO,从而提高查询的效率。这没问题,因为这种情况下只使用了这张表只有主键索引,没有任何其他的列进行查询
- 如果还有类似如下的SQL语句:SELECT* FROM TABLE WHERE KEY=@key,这时对于KEY的查询需要扫描所有的10个分区,即使每个分区的查询开销为2次IO,则一共需要20次IO。而对于原来单表的设计,对于KEY的查询只需要2~3次IO
演示案例
- 创建下面一个表,根据主键ID进行HASH分区。分区的数量为10,并且表中有接近十万行数据
- 因为是HASH分区,所以每个分区的记录数大致都是相同的,比价均匀
- 注意:即使是根据自增长主键进行的HASH分区也不能 保证分区数据的均匀。因为插入的自增长ID并非总是连续的,如果该主键值因为某种原因回滚了,则该值将不会再次被自动使用
- 如果进行主键的查询,可以发现分区的确是有意义的,可以看见值寻找了p1分区
- 但是如果根据表中的nickname列索引济宁查询,explain partitions会得到下面的结果,可以看到,MySQL数据库会搜索所有分区:
- 然后进行查询,可以看到简单的索引查询语句竟然使用了1.05秒,这显然是因为查询需要遍历所有的分区的关系,实际的IO执行了约20~30此,而在未分区的同样表结构大小的表上,执行下面的语句只需要0.26秒
- 根据上面的演示案例,我们知道:对于InnoDB存储引擎作为OLTP应用的表在使用分区时应该十分小心,设计时确认数据的访问模式;否则在OLTP应用下分区可能不仅不会带来查询速度的提高,反而可能会使你的应用执行的更慢
四、在表和分区间交换数据
- MySQL 5.6开始支持“alter table ...... exchange partition”语法。该语句允许分区或子分区中的数据与另一个非分区的表中的数据进行交换:
- 如果非分区表中的数据为空,相当于将分区中的数据移到非分区表中
- 如果分区表中的数据为空,相当于将外部表的数据导入到分区中
- 在表和分区间交换数据,必须满足下面的条件:
- 要交换的表需和分区表有着相同的表结构,但是表不能含有分区
- 在非分区表中的数据必须在交换的分区定义内
- 被交换的表不能包含有外键,或有其他的表含有对该表的外键作用
- 用户除了需要ALTER、INSERT、CREATE权限外,还需要DROP的权限
- 还有两个注意事项:
- 交换表,不会触发交换表和被交换表上的触发器
- AUTO_INCREMENT列被重置
演示案例
- 创建一个表,对字段id进行RANGE分区
create table e( id int not null, fname varchar(30), lname varchar(30) )engine=innodb partition by range(id)( partition p0 values less than (50), partition p1 values less than (100), partition p2 values less than (150), partition p3 values less than maxvalue );
- 向表中插入数据
insert into e values (1669,"Jim","Smith"), (337,"Mary","Jones"), (16,"Frank","White"), (2005,"Linda","Black");
- 创建一个表(该表与上面的分区表要有一样的结构,因此使用下面的语句比较快接)
create table e2 like e;
- 删除所有的分区
alter table e2 remove partitioning;
- 查看分区表的数据
select partition_name,table_rows from information_schema.partitions where table_name='e';
- 现在e2中没有数据,使用下面的语句将e表分区p0中的数据交换到e2表中
alter table e exchange partition p0 with table e2;
- 这是查看e表中的数据,发现p0分区中的数据已经没有了
select partition_name,table_rows from information_schema.partitions where table_name='e';
- 再次查看e2表,发现数据移动到这个表中了