- 分区表并不是在存储引擎层完成的,因此不是只有InnoDB存储引擎支持分区,常见的存储引擎MyISAM、NDB等都支持。但也并不是所有的存储引擎都支持,如CSV、FEDORATED、MERGE等就不支持
- MySQL在5.1版本时添加了对分区的支持。分区的过程是将一个表或索引分解为多个更小、更可管理的部分。就访问数据库的应用而言,从逻辑上讲,只有一个表或一个索引,但是在物理上这个表或索引可能由数十个物理分区组成。每个分区都是独立的对象,可以独自处理,也可以作为一个更大对象的部分进行处理
水平分区、垂直分区
- 水平分区:指将同一表中不同行的记录分配到不同的物理文件中
- 垂直分区:指将同一表中不同列的记录分配到不同的物理文件中
- MySQL数据库支持的分区类型为水平分区,并不支持垂直分区
局部分区索引、全局分区
- 此外,MySQL数据库的分区是局部分区索引,一个分区中既存放了数据又存放了索引。而全局分区是指,数据存放在各个分区中,但是所有数据的索引放在一个对象中。目前,MySQL数据库还不支持全局分区
- have_partitioning参数:用来控制数据库是否开启分区功能。默认为开启
- 也可以通过SHOW PLUGINS来查看:
对分区的一个误解:
- 大多数DBA认为:只要启用了分区,数据库就会运行的更快。这个说法是不正确的
- 分区可能会给某些SQL语句性能代替提高,但是分区主要用于数据库高可用性的管理。在OLTP应用中,对于分区的使用应该非常小心。总之,如果只是一味地使用分区,而不理解分区是如何工作的,也不清楚你的应用如何使用分区,那么分区有可能对性能产生负面的影响
分区的类型
- RANGE分区:行数据基于属于一个给定连续区间的列值被放入分区。MySQL 5.5开始支持RANGE COLUMNS的分区
- LIST分区:和RANGE分区类型类似,只是LIST分区面向的是离散的值。MySQL 5.5开始支持LIST COLUMNS的分区
- HASH分区:根据用户自定义的表达式的返回值来进行分区,返回值不能为负数
- KEY分区:根据MySQL数据库提供的哈希函数来进行分区
创建分区的限制条件
- 不论创建何种类中的分区,如果表中存在主键或唯一索引时,分区列必须是唯一索引的一个组成部分,因此下面创建分区的SQL语句会产生错误:
create table t1( col1 int not null, col2 date not null, col3 int not null, col4 int not null, unique key(col1,col2) )partition by hash(col3) partitions 4;
- 唯一索引可以是允许NULL值的,并且分区列只要是唯一索引的一个组成部分,不需要整个唯一索引列都是分区列,如:
create table t1( col1 int not null, col2 date not null, col3 int not null, col4 int not null, unique key(col1,col2,col3,col4) )partition by hash(col3) partitions 4;
- 如果建表时没有指定主键/唯一索引,可以指定任何一个列为分区列,因此下面两句创建分区的SQL语句都可以正确运行:
create table t2( col1 int not null, col2 date not null, col3 int not null, col4 int not null )engine=innodb partition by hash(col3) partitions 4;
create table t3( col1 int not null, col2 date not null, col3 int not null, col4 int not null, key(col4) )partition by hash(col3) partitions 4;
一、RANGE分区
- RANGE是最常用的一种分区类型
分区的基本使用(附maxvalue)
- 根据id列进行分区:
- 当id小于10(不包括10)时,数据插入p0分区
- 当id大于等于10小于20时,数据插入p1分区
create table t( id int )engine=innodb partition by range(id)( partition p0 values less than(10), partition p1 values less than(20) );
- 之后查看表在磁盘上的物理文件,启动分区之后,表不再由一个.ibd文件组成了,而是由建立分区时的各个分区.ibd文件组成。表t根据列id进行分区,所以数据时根据列id的值的范围存放在不同的物理文件中的。
- 插入3行数据:
insert into t select 9; insert into t select 10; insert into t select 15;
- 可以通过查询information_schema数据库下的partitions表来查询每个分区的具体信息:
- TABLES_ROWS:反映了每个分区中记录的数量。可以看到分区p0有1条记录,分区p1有2条记录
- PARTITION_METHOD:表示分区类型,这里显示的是RANGE
select * from information_schema.partitions where table_schema=database() and table_name='t'\G;
- 当我们插入一个不在分区内的值时,分区会抛出一个异常。例如,我们向表t中插入一个值30
insert into t select 30;
- 针对于上面的问题,我们可以为分区添加一个MAXVALUE分区。MAXVALUE可以理解为正无穷,因此所有大于等于20且小于MAXVALUE的值放入在其他分区。修改之后再次插入超过20的值不会报错:
alter table t add partition( partition p2 values less than maxvalue ); insert into t select 30;
- 再次查看可以发现多了一个分区p2,并且分区中有一行数据
对分区进行数据查找、删除
- RANGE分区主要用于日期列的分区
- 例如对于销售类的表,可以根据年份来分区存放销售记录,如下面的分区表sales:
-- 因为是根据年份,因此range()中使用了year函数获取datetime数据类型中的年份 create table sales( money int unsigned not null, date datetime )engine=innodb partition by range(year(date))( partition p2008 values less than (2009), partition p2009 values less than (2010), partition p2010 values less than (2011) );
- 然后插入几行数据
-- 存入分区p2008 insert into sales select 100,'2008-01-01'; -- 存入分区p2008 insert into sales select 100,'2008-02-01'; -- 存入分区p2008 insert into sales select 200,'2008-01-02'; -- 存入分区p2009 insert into sales select 100,'2009-03-01'; -- 存入分区p2010 insert into sales select 200,'2010-01-01';
- 查看分区表:
select partition_name,table_rows from information_schema.partitions where table_schema=database() and table_name='sales';
- 如果我们想要查询2008年整年的销售额,只需要执行下面的语句即可(出去explain partitions,这个只是用来测试后面语句的执行情况的),可以看到select命令只需要去搜索p2088这个分区即可(见图中的partitions字段),而不会去搜索所有的分区,因此查询的速度得到了大幅度的提升:
explain partitions select * from sales where date>='2008-01-01' and date<='2008-12-31';
- 但是如果我们执行下列语句,虽然结果与上面的相同,但是效率不同。下面把and后面的条件从date<='2008-12-31'改为了date<='2009-01-01',此时回去搜索p2008和p2009两个分区(见图中的partitions字段),因此这种方法是低效的
explain partitions select * from sales where date>='2008-01-01' and date<='2009-01-01';
- 因此我们需要编写可以根据分区的特性最优的SQL语句
- 如果我们要删除2008年的数据,不需要执行(delete from sales where date>='2008-01-01' and date<'2009-01-01'),只需要执行下面的语句删除2008年数据所在的分区即可
alter table sales drop partition p2008;
分区函数的使用注意事项
- 接着上面的sales表,在上面我们使用year函数对date字段进行分区,但是我们还可以根据每年每月来进行分区
- 例如,下面以date字段来以年月进行分区
create table sales( money int unsigned not null, date datetime )engine=innodb partition by range(year(date)*100+month(date))( partition p201001 values less than (201002), partition p201002 values less than (201003), partition p201003 values less than (201004) );
- 我们测试一下查询语句的效率:从下面可以看到我们去查询2010年的数据,但是partitions字段显示SQL语句去查询了每一个分区,效率比较低
explain partitions select * from sales where date>='2010-01-01' and date<='2010-01-31';
- 产生上面的原因是对于range分区的查询,优化器只能对year()、to_days()、to_seconds()、unix_timestamp()这类函数进行优化选择。因此需要重新定义一张表,改用分区函数to_days()。见下:
drop table if exists sales; create table sales( money int unsigned not null, date datetime )engine=innodb partition by range(to_days(date))( partition p201001 values less than (to_days('2010-02-01')), partition p201002 values less than (to_days('2010-03-01')), partition p201003 values less than (to_days('2010-04-01')) );
- 现在我们进行上面相同的查询,可以看到只对p201001分区进行了查询,达到了我们的要求:
explain partitions select * from sales where date>='2010-01-01' and date<='2010-01-31';
二、LIST分区
- LIST分区与RANGE分区类似,只不过LIST分区的值是离散的,而不是连续的
- 因为离散的,因此定义时需要使用“in”关键字而不是“less than”关键字
- 如果插入不存在于表中的记录时,与RANGE一样也会抛出异常
演示案例
- 创建一张表,分两个区:
create table t( a int, b int )engine=innodb partition by list(b)( partition p0 values in (1,3,5,7,9), partition p1 values in (0,2,4,6,8) );
- 然后向表中插入一些数据:
insert into t select 1,1; insert into t select 1,2; insert into t select 1,3; insert into t select 1,4;
- 然后查看分区的状态,可以看到p0、p1分区各有2行数据:
select table_name,partition_name,table_rows from information_schema.partitions where table_schema=database() and table_name='t';
插入出错时,不同存储引擎的处理(有错误,待更新)
- 插入多行数据时遇到错误,不同存储引擎的处理也不同:
- MyISAM会将错误插入语句之前的数据都插入,之后的不插入
- InnoDB会将其视为一个事务,因此所有的插入语句都失败
- 接着上面的t表,我们先将其中的数据都删除,然后重新插入多行数据,可以看到InnoDB存储引擎没有插入任何数据:
truncate table t; -- 其中(6,10)出错,导致所有都没有插入 insert into t values (1,2),(2,4),(6,10),(5,3); select * from t;
- 下面将表更改为MyIASML类型,然后重新进行实验,但是没有预期的效果(这个地方有错误,可能是MySQL版本没有更新或者是更改了这个功能,待续更新)
alter table t engine=MyIASM; -- 其中(6,10)出错,(1,2),(2,4)应该可以插入成功 insert into t values (1,2),(2,4),(6,10),(5,3); select * from t;
三、HASH分区
- HASH分区的目的:是将数据均匀地分布到预先定义的各个分区中,保证各分区的数据数量大致都是一样的
- 在RANGE和LIST分区中,必须明确指定一个给定的列值或列值集合应该保存在哪个分区中;而在HASH分区中,MySQL自动完成这些工作,用户所要做的只是基于将要进行哈希分区的列值指定一个列值或表达式,以及指定被分区的表将要被分割的分区数量
- 使用方法:
- 要使用HASH分区来分割一个表,要在create table语句上添加一个“partition by hash(expr)”子句,其中“expr”是一个返回一个整数的表达式。它可以仅仅是字段类型为MySQL整型的列名
- 此外,用户需要在后面再添加一个“partitions mum”子句,其中mum是一个非负的整数,它表示表将要被分割成分区的数量。如果没有,那么分区的数量将默认为1
演示案例
- 下面创建一个HASH分区的表t_hash,分区按日期列b进行分区,并且分为4个区:
create table t_hash( a int, b datetime )engine=InnoDB partition by hash (year(b)) partitions 4;
- 因为上面我们分了4个区,所以例如我们插入一个“2010-04-01”记录,那么该记录会被保存在分区2中,计算方法如下:
mod(year('2010-04-01'),4) =mod(2010,4) =2
- 现在我们插入“2010-04-01”记录,然后检测一下是否插入到分区2中,结果一致:
insert into t_hash select 1,'2010-04-01'; select table_name,partition_name,table_rows from information_schema.partitions where table_schema=database() and table_name='t_hash';
- 因为分区是按照year函数进行的,而这个值本身可是离散的。如果对于连续的值进行HASH分区,如自增长的主键,则可以较好地将数据进行平均分布
LINEAR HASH分区
- MySQL数据库还支持一种称为LINEAR HASH的分区,它使用一个更加复杂的算法来确定新行插入到已经分区的表中的位置。它的语法和HASH分区的语法相似,只是将关键字HASH改为LINEAR HASH
- LINEAR HASH的优缺点:
- 优点:增加、删除、合并和拆分分区将变得更加快捷,这有利于处理含有大量数据的表
- 缺点:与使用HASH分区得到的数据分布相比,各个分区间数据的分布可能不大均衡
- 下面创建一个LINEAR HASH的分区表t_linear_hash,它和之前的表t_hash相似,只是分区类型不同:
create table t_linear_hash( a int, b datetime )engine=InnoDB partition by linear hash (year(b)) partitions 4;
- 同样是插入“2010-04-01”的记录,这次MySQL数据库根据以下的方法来进行分区判断:
- 取大于分区数量4的下一个2的幂值V,V=POWER(2,CEILING(LOG(2,mum)))=4
- 所在分区N=YEAR('2010-04-01')&(V-1)=2
- 虽然还是在分区P2中,但是计算的方法和之前的HASH分区完全不同,接着进行插入实际数据的验证:
insert into t_linear_hash select 1,'2010-04-01'; select table_name,partition_name,table_rows from information_schema.partitions where table_schema=database() and table_name='t_linear_hash';
四、KEY分区
- KEY分区和HASH分区相似,不同之处在于HASH分区使用用户自定义的进行分区,KEY分区使用MySQL数据库提供的函数进行分区:
- 对于NDB Cluster引擎,MySQL数据库使用MD5函数来分区
- 对于其他存储引擎,MySQL使用其内部的哈希函数,这些函数基于与PASSWORD()一样的运算法则
演示案例
- 下面创建一个表,进行KEY分区,分4个区
create table t_key( a int, b datetime )engine=InnoDB partition by key (b) partitions 4;
LINEAR KEY分区
- 在KEY中使用关键字LINEAR分区和在HASH分区中使用具有同样的效果(见上面的LINEAR HASH分区)
五、COLUMNS关键字
- 在前面介绍的RANGE、LIST、HASH、KEY分区,分区的条件是:数据必须是整型,如果不是整型,那应该需要通过函数将其转换为整型,如year()、to_days()、month()等函数
- MySQL 5.5版本开始支持COLUMNS分区,可视为RANGE分区和LIST分区的一种进化。COLUMNS分区可以直接使用非整型的数据进行分区,分区根据类型直接比较而得,不需要转换为整型
- 此外,RANGE COLUMNS分区可以对多个列的值进行分区
- COLUMNS分区支持与不支持以下的数据类型:
- 所有的整型类型:如int、smallint、tinyint、bigint。不支持float、decimal
- 日期类型:如date和datetime。其余的日志类型不予支持
- 字符串类型:如char、varchar、binary、varbinary。不支持blob、text类型
演示案例
- 因为COLUMNS分区不需要进行整型的转换了,所有我们也就不需要year()、to_days()这些函数了,可以直接使用COLUMNS
- 下面创建一个表,不再需要对b字段使用year函数
create table t_columns_range( a int, b datetime )engine=innodb partition by range columns (b) partition p0 values less than ('2009-01-01'), partition p1 values less than ('2010-01-01') );
演示案例
- 同样可以直接使用字符串的分区:
create table customers_1( first_name varchar(25), last_name varchar(25), street_1 varchar(30), street_2 varchar(30), city varchar(15), renewal date )partition by list columns(cist)( partition pRegion_1 values in('Oskarshamn','Hogsby','Monsteras'), partition pRegion_2 values in('Vimmerby','Hultsfred','Vastervik'), partition pRegion_3 values in('Nassjo','Eksjo','Vetlanda'), partition pRegion_4 values in('Uppvidinge','Alvesta','Vaxjo') );
演示案例
- 也可以对多列进行分区,例如:
create table rcx( a int, b int, c char(3), d int )engine=innodb partition by range columns(a,d,c)( partition p0 values less than (5,10,'ggg'), partition p1 values less than (10,20,'mmmm'), partition p2 values less than (15,30,'sss'), partition p3 values less than (maxvalue,maxvalue,maxvalue), );