一、MySQL分区表介绍
分区是一种表的设计模式,正确的分区可以极大地提升数据库的查询效率,完成更高质量的SQL编程。但是如果错误地使用分区,那么分区可能带来毁灭性的的结果。
分区功能并不是在存储引擎层完成的,因此不只有InnoDB存储引擎支持分区,常见的存储引擎MyISAM、NDB等都支持分区。但是并不是所有的存储引擎都支持,如CSV、FEDORATED、MERGE等就不支持分区。在使用此分区功能前,应该对选择的存储引擎对分区的支持有所了解。
MySQL数据库在5.1版本时添加了对分区的支持,分区的过程是将一个表或索引分解为多个更小、更可管理的部分。就访问数据库的应用而言,从逻辑上讲,只有一个表或一个索引,但是在物理上这个表或索引可能由数十个物理分区组成。每个分区都是独立的对象,可以独自处理,也可以作为一个更大对象的一部分进行处理。
MySQL数据库支持的分区类型为水平分区(指将同一个表中不同行的记录分配到不同的物理文件中),并不支持垂直分区(指将同一表中不同列的记录分配到不同的物理文件中)。此外,MySQL数据库的分区是局部分区索引,一个分区中既存放了数据又存放了索引。而全局分区是指,数据存放在各个分区中,但是所有数据的索引放在一个对象中。目前,MySQL数据库还不支持全局分区。
可以通过以下命令来查看当前数据库是否启用了分区功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | mysql> show global variables like '%partition%'; +-------------------+-------+ | Variable_name | Value | +-------------------+-------+ | have_partitioning | YES | +-------------------+-------+ 1 row in set (0.04 sec)
mysql> show plugins\G *************************** 43. row *************************** Name: partition Status: ACTIVE Type: STORAGE ENGINE Library: NULL License: GPL |
有时候可能会有这么一种误区,只要启用了分区,数据库就会运行的更快。这个结论结论是存在很多问题的,就经验来看,分区可能会给某些SQL语句性能带来提高,但是分区主要用于数据库高可用性的管理。在OLTP应用中,对于分区的使用应该非常小心,总之,如果只是一味地使用分区,而不理解分区是如何工作的,也不清楚你的应用如何使用分区,那么分区极有可能会对性能产生负面的影响。
二、MySQL分区类型
1)RANGE分区
我们介绍的第一种分区类型是RANGE分区,也是最常用的一种分区类型,基于属于一个给定连续区间的列值,把多行分配给分区。
这些区间要连续且不能相互重叠,使用VALUES LESS THAN操作符来进行定义。下面创建一个以id列为区间分区表,当id小于10时,数据插入p0分区;当id大于等于10小于20时,数据插入p1分区。
1 2 3 4 5 6 7 | createtablet( idint )engine=innodb partitionbyrange(id)( partitionp0valueslessthan(10), partitionp1valueslessthan(20) ); |
查看表在磁盘上的物理文件,启用分区之后,表不再由一个ibd文件组成了。而是由建立分区时的各个分区ibd文件组成。
1 2 3 4 5 6 | mysql> system ls -hl /data/mysql/3306/data/db/ total 208K -rw-r----- 1 mysql mysql 67 Feb 27 12:03 db.opt -rw-r----- 1 mysql mysql 8.4K Feb 27 12:07 t.frm -rw-r----- 1 mysql mysql 96K Feb 27 12:07 t#P#p0.ibd -rw-r----- 1 mysql mysql 96K Feb 27 12:07 t#P#p1.ibd |
接着插入如下数据:
1 2 3 | mysql>insertintotselect9; mysql>insertintotselect10; mysql>insertintotselect15; |
因为表t根据id进行分区,所以数据是根据列id的值的范围存放在不同的物理文件中的,可以通过查询information_schema架构下的partitions表来查看每个分区的具体信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | mysql> select * from information_schema.partitions where table_schema=database() and table_name='t'\G *************************** 1. row *************************** TABLE_CATALOG: def TABLE_SCHEMA: db TABLE_NAME: t PARTITION_NAME: p0 SUBPARTITION_NAME: NULL PARTITION_ORDINAL_POSITION: 1 SUBPARTITION_ORDINAL_POSITION: NULL PARTITION_METHOD: RANGE SUBPARTITION_METHOD: NULL PARTITION_EXPRESSION: id SUBPARTITION_EXPRESSION: NULL PARTITION_DESCRIPTION: 10 TABLE_ROWS: 1 AVG_ROW_LENGTH: 16384 DATA_LENGTH: 16384 MAX_DATA_LENGTH: NULL INDEX_LENGTH: 0 DATA_FREE: 0 CREATE_TIME: 2017-02-27 12:07:29 UPDATE_TIME: 2017-02-27 13:05:05 CHECK_TIME: NULL CHECKSUM: NULL PARTITION_COMMENT: NODEGROUP: default TABLESPACE_NAME: NULL *************************** 2. row *************************** TABLE_CATALOG: def TABLE_SCHEMA: db TABLE_NAME: t PARTITION_NAME: p1 SUBPARTITION_NAME: NULL PARTITION_ORDINAL_POSITION: 2 SUBPARTITION_ORDINAL_POSITION: NULL PARTITION_METHOD: RANGE SUBPARTITION_METHOD: NULL PARTITION_EXPRESSION: id SUBPARTITION_EXPRESSION: NULL PARTITION_DESCRIPTION: 20 TABLE_ROWS: 2 AVG_ROW_LENGTH: 8192 DATA_LENGTH: 16384 MAX_DATA_LENGTH: NULL INDEX_LENGTH: 0 DATA_FREE: 0 CREATE_TIME: 2017-02-27 12:07:29 UPDATE_TIME: 2017-02-27 13:05:12 CHECK_TIME: NULL CHECKSUM: NULL PARTITION_COMMENT: NODEGROUP: default TABLESPACE_NAME: NULL 2 rows in set (0.00 sec) |
TABLE_ROWS列反映了每个分区中记录的数量。由于之前向表中插入了9、10、15三条记录,因此可以看到,当前分区p0中有1条记录,p1分区中有两条记录。PARTITION_METHOD表示分区的类型,这里显示的是RANGE。
对于表t,由于我们定义了分区,因此对于插入的值应该严格遵守分区的定义,当插入一个不在分区中定义的值时,MySQL数据库会抛出一个异常。如下所示:
1 2 | mysql>insertintotselect30; ERROR1526(HY000):Tablehasnopartitionforvalue30 |
对于上述问题,我们可以对分区添加一个MAXVALUE值的分区,MAXVALUE可以理解为正无穷,因此所有大于等于20且小于MAXVALUE的值被放入p2分区。
1 2 3 4 5 6 7 | mysql> alter table t add partition(partition p2 values less than maxvalue); Query OK, 0 rows affected (0.02 sec) Records: 0 Duplicates: 0 Warnings: 0
mysql> insert into t select 30; Query OK, 1 row affected (0.00 sec) Records: 1 Duplicates: 0 Warnings: 0 |
RANGE分区主要用于日期列的分区,例如对于销售类的表,可以根据年来分区存放销售记录,如下面的分区表sales。
1 2 3 4 5 6 7 8 | createtablesales( moneyintunsignednotnull, datedatetime)engine=innodb partitionbyrange(year(date))( partitionp2014valueslessthan(2015), partitionp2015valueslessthan(2016), partitionp2016valueslessthan(2017) ); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | mysql> insert into sales select 100,'2013-01-01'; Query OK, 1 row affected (0.00 sec) Records: 1 Duplicates: 0 Warnings: 0
mysql> insert into sales select 100,'2013-02-01'; Query OK, 1 row affected (0.01 sec) Records: 1 Duplicates: 0 Warnings: 0
mysql> insert into sales select 200,'2013-01-02'; Query OK, 1 row affected (0.00 sec) Records: 1 Duplicates: 0 Warnings: 0
mysql> insert into sales select 200,'2014-03-01'; Query OK, 1 row affected (0.00 sec) Records: 1 Duplicates: 0 Warnings: 0
mysql> insert into sales select 100,'2015-03-01'; Query OK, 1 row affected (0.00 sec) Records: 1 Duplicates: 0 Warnings: 0 |
这样创建的好处就是便于对sales这张表的管理。如果我们要删除2015年的数据,不需要执行delete from sales where date>=’2015-01-01′ and dater<= ‘2016-01-01’,只需要删除2015年所在的分区即可。
1 2 3 | mysql>altertablesalesdroppartitionp2015; QueryOK,0rowsaffected(0.01sec) Records:0 Duplicates:0 Warnings:0 |
这样创建的另一个好处就是可以加快某些查询操作,如果我们只需要查询2015年整年的销售额,可以这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | mysql> explain partitions select * from sales where date>='2014-01-01' and date<='2014-12-31'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: sales partitions: p2014 type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 4 filtered: 25.00 Extra: Using where 1 row in set, 2 warnings (0.00 sec) |
通过explain partitions命令我们可以发现,在上述语句中,SQL优化只需要去搜索p2014这个分区,而不会去搜索所有的分区,称为分区修剪(partition pruning),故查询的速度得到了大幅度的提升。需要注意的是,如果执行下列语句,结果是一样的,但是优化器的选择可能又会不同了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | mysql>explainpartitionsselect*fromsaleswheredate>='2014-01-01'anddate<='2015-01-01'\G ***************************1.row*************************** id:1 select_type:SIMPLE table:sales partitions:p2014,p2016 type:ALL possible_keys:NULL key:NULL key_len:NULL ref:NULL rows:4 filtered:25.00 Extra:Usingwhere 1rowinset,2warnings(0.00sec) |
这次条件改为date<=’ 2015-01-01’而不是date<=’2014-12-31’时,优化器会选择搜索两个分区,这是我们不希望看到的,因此对于启用分区,应该根据分区的特性来编写最优的SQL。
在进行分区时,如果出现“This partition function is not allowed”的错误提示,则你可能使用了非支持函数。MySQL 5.6支持的partition函数:http://dev.mysql.com/doc/refman/5.6/en/partitioning-limitations-functions.html
另外,需要注意一点的时,对于RANGE分区的查询,优化器只能对YEAR(),TO_YEAR(),TO_SECONDS(),UNIX_TIMESTAMP()这类函数进行优化选择,如果你使用了其他的函数或方法编写了符合分区特性的SQL语句,就不一定能够使用查询优化,所以再编写SQL语句时尽量测试。
2)LIST分区
LIST分区和RANGE分区类似,区别在于LIST分区是基于列值匹配一个离散值集合中的某个值来进行选择,而非连续的。
LIST分区通过使用“PARTITION BY LIST(expr)”来实现,其中“expr” 是某列值或一个基于某个列值、并返回一个整数值的表达式,然后通过“VALUES IN (value_list)”的方式来定义每个分区,其中“value_list”是一个通过逗号分隔的整数列表。
1 2 3 4 5 6 7 8 9 | CREATE TABLE employees ( id INT NOT NULL, fname VARCHAR(30), lname VARCHAR(30), hired DATE NOT NULL DEFAULT '1970-01-01', separated DATE NOT NULL DEFAULT '9999-12-31', job_code INT, store_id INT ); |
假定有20个音像店,分布在4个有经销权的地区,如下表所示:
1 2 3 4 5 6 7 8 | ========================== 地区 商店ID号 -------------------------- 北区 3,5,6,9,17 东区 1,2,10,11,19,20 西区 4,12,13,14,18 中区 7,8,15,16 ========================== |
不同于RANGE分区中定义的VALUES LESS THAN语句,LIST分区使用VALUES IN,因为每个分区的值是离散的,因此只能定义值。按照属于同一个地区商店的行保存在同一个分区中的方式来分割表,可以使用下面的“CREATE TABLE”语句:
1 2 3 4 5 6 7 8 9 10 11 12 13 | CREATE TABLE employees ( id INT NOT NULL, name VARCHAR(30), hired DATE NOT NULL DEFAULT '1970-01-01', separated DATE NOT NULL DEFAULT '9999-12-31', store_id INT ) PARTITION BY LIST(store_id) PARTITION pNorth VALUES IN (3,5,6,9,17), PARTITION pEast VALUES IN (1,2,10,11,19,20), PARTITION pWest VALUES IN (4,12,13,14,18), PARTITION pCentral VALUES IN (7,8,15,16) ); |
这使得在表中增加或删除指定地区的雇员记录变得容易起来。例如,假定西区的所有音像店都卖给了其他公司。那么与在西区音像店工作雇员相关的所有记录(行)可以使用“ALTER TABLE employees DROP PARTITION pWest;”来进行删除,它与具有同样作用的DELETE (删除)查询“DELETE query DELETE FROM employees WHERE store_id IN (4,12,13,14,18);”比起来,要有效得多。
【要点】如果试图插入列值(或分区表达式的返回值)不在分区值列表中的一行时,那么“INSERT”查询将失败并报错。例如,假定LIST分区的采用上面的方案,下面的查询将失败:
1 | INSERTINTOemployeesVALUES(224,'Linus','2015-05-01','2015-10-12',42,21); |
这是因为“store_id”列值21不能在用于定义分区pNorth, pEast, pWest,或pCentral的值列表中找到。要重点注意的是,LIST分区没有类似如“VALUES LESS THAN MAXVALUE”这样的包含其他值在内的定义。将要匹配的任何值都必须在值列表中找到。
LIST分区除了能和RANGE分区结合起来生成一个复合的子分区,与HASH和KEY分区结合起来生成复合的子分区也是可能的。
3)HASH分区
HASH分区的目的是将数据均匀地分布到预先定义的各个分区中,保证各分区的数据量大致都是一样的。在RANGE和LIST分区中,必须明确指定一个给定的列值或列值集合应该保存在哪个分区中;而在HASH分区中,MySQL自动完成这些工作,用户所要做的只是基于将要进行哈希分区的列值指定一个列值或表达式,以及指定被分区的表将要被分隔成的分区数量。
要使用HASH分区来分割一个表,要在CREATE TABLE 语句上添加一个“PARTITION BY HASH (expr)”子句,其中“expr”是一个返回一个整数的表达式。它可以仅仅是字段类型为MySQL 整型的一列的名字。此外,你很可能需要在后面再添加一个“PARTITIONS num”子句,其中num是一个非负的整数,它表示表将要被分割成分区的数量,如果没有包括一个PARTITIONS子句,那么分区的数量将默认为1。
1 2 3 4 5 6 7 | CREATE TABLE employees_h ( id INT NOT NULL, lname VARCHAR(30), store_id INT ) PARTITION BY HASH(store_id) PARTITIONS 4; |
如果插入一个列store_id为10,20,30的值,那么保存该条记录的分区如下:
1 2 3 4 5 6 7 8 9 10 | mysql>selectTABLE_ROWSfrominformation_schema.partitionswheretable_schema=database()andtable_name='employees_h'\G ***************************1.row*************************** TABLE_ROWS:1 ***************************2.row*************************** TABLE_ROWS:0 ***************************3.row*************************** TABLE_ROWS:2 ***************************4.row*************************** TABLE_ROWS:0 4rowsinset(0.00sec) |
可以看到插入的数据正是按照mod(values,4)这种方式来进行插入的。如果对于连续的值进行HASH分区,如自增主键,则可以较好地将数据进行平均分布。
4)LINER HASH
MySQL还支持线性哈希功能,它与常规哈希的区别在于,线性哈希功能使用的一个线性的2的幂(powers-of-two)运算法则,而常规哈希使用的是求哈希函数值的模数。
线性哈希分区和常规哈希分区在语法上的唯一区别在于,在“PARTITION BY” 子句中添加“LINEAR”关键字。
1 2 3 4 5 6 7 8 9 | CREATE TABLE employees_lh ( id INT NOT NULL, name VARCHAR(30), hired DATE NOT NULL DEFAULT '1970-01-01', separated DATE NOT NULL DEFAULT '9999-12-31', store_id INT ) PARTITION BY LINEAR HASH(YEAR(hired)) PARTITIONS 4; |
按照线性哈希分区的优点在于增加、删除、合并和拆分分区将变得更加快捷,有利于处理含有极其大量(1000吉)数据的表。它的缺点在于,与使用
常规HASH分区得到的数据分布相比,各个分区间数据的分布不大可能均衡。
5)KEY分区
KEY分区和HASH分区相似,不同之处在于HASH分区使用用户定义的函数进行分区,支持字符串HASH分区,KEY分区使用MySQL数据库提供的函数进行分区,这些函数基于与PASSWORD()一样的运算法则。
1 2 3 4 5 6 7 | CREATETABLEtk( col1INTNOTNULL, col2CHAR(5), col3DATE ) PARTITIONBYLINEARKEY(col1) PARTITIONS3; |
在KEY分区中使用关键字LINEAR和在HASH分区中使用具有同样的作用,分区的编号是通过2的幂(powers-of-two)算法得到,而不是通过模数算法。
6)COLUMNS
在前面说了RANGE、LIST、HASH和KEY这四种分区中,分区的条件是:数据必须为整形(interger),如果不是整形,那应该需要通过函数将其转化为整形,如YEAR(),TO_DAYS(),MONTH()等函数。MySQL5.5版本开始支持COLUMNS分区,可视为RANGE分区和LIST分区的一种进化。COLUMNS分区可以直接使用非整形的数据进行分区,分区根据类型直接比较而得,不需要转化为整形。此外,RANGE COLUMNS分区可以对多个列的值进行分区。
COLUMNS分区支持以下的数据类型:
- 所有的整形类型,如INT、SMALLINT、TINYINT和BIGINT。而FLOAT和DECIMAL则不予支持。
- 日期类型,如DATE何DATETIME。其余的日期类型不予支持。
- 字符串类型,如CHAR、VARCHAR、BINARY和VARBINARY。而BLOB和TEXT类型不予支持。
对于日期类型的分区,我们不再需要YEAR()和TO_DATS()函数了,而直接可以使用COLUMNS,如:
1 2 3 4 5 6 7 8 9 | CREATE TABLE `t_c` ( `key` varchar(50), `value` varchar(50), `create_time` datetime ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 PARTITION BY RANGE COLUMNS (create_time) ( PARTITION p0 VALUES LESS THAN ('2017-01-01 00:00:00'), PARTITION p1 VALUES LESS THAN ('2017-03-01 00:00:00') ); |
同样可以使用字符串分区。
1 2 3 4 5 6 7 8 9 10 | CREATETABLE`monitor_2`( `key`varchar(15), `value`varchar(50), `create_time`datetime, `city`VARCHAR(15) )ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 PARTITIONBYLISTCOLUMNS(city)( PARTITIONp0VALUESIN('shanghai','beijing','shenzhen'), PARTITIONp1VALUESIN('hubei','henan','hunan') ); |
对比RANGE分区和LIST分区,Columns分区的亮点除了支持数据类型增加之外,另外一大亮点是Columns分区还支持多列分区。如:
1 2 3 4 5 6 7 8 9 10 11 | CREATE TABLE `monitor_3` ( `key` varchar(15), `value` varchar(50), `create_time` datetime, `test` VARCHAR(1) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 PARTITION BY RANGE COLUMNS(create_time,test) ( PARTITION p0 VALUES LESS THAN ('2017-01-01 00:00:00','yes'), PARTITION p1 VALUES LESS THAN ('2017-03-01 00:00:00','no'), PARTITION p2 VALUES LESS THAN (MAXVALUE,MAXVALUE) ); |
MySQL 5.5开始支持COLUMNS分区,对于之前的RANGE和LIST分区,用户可以用RANGE COLUMNS和LIST COLUMNS分区进行很好的代替了。
三、MySQL子分区
子分区(subparttitioning)是在分区的基础上再进行分区,有时也称这种分区为复合分区。MySQL数据库允许在RANGE和LIST的分区上再进行HASH或KEY的子分区,如:
1 2 3 4 5 6 7 8 | createtablets(aint,bdate)engine=innodb partitionbyrange(year(b)) subpartitionbyhash(to_days(b)) subpartitions2( partitionp0valueslessthan(1990), partitionp1valueslessthan(2000), partitionp2valueslessthanmaxvalue ); |
1 2 3 4 5 6 7 8 9 10 | mysql> system ls -ln /data/mysql/3306/data/db total 592 -rw-r----- 1 27 27 67 Feb 27 12:03 db.opt -rw-r----- 1 27 27 8578 Feb 27 15:54 ts.frm -rw-r----- 1 27 27 98304 Feb 27 15:54 ts#P#p0#SP#p0sp0.ibd -rw-r----- 1 27 27 98304 Feb 27 15:54 ts#P#p0#SP#p0sp1.ibd -rw-r----- 1 27 27 98304 Feb 27 15:54 ts#P#p1#SP#p1sp0.ibd -rw-r----- 1 27 27 98304 Feb 27 15:54 ts#P#p1#SP#p1sp1.ibd -rw-r----- 1 27 27 98304 Feb 27 15:54 ts#P#p2#SP#p2sp0.ibd -rw-r----- 1 27 27 98304 Feb 27 15:54 ts#P#p2#SP#p2sp1.ibd |
表ts先根据b列进行了RANGE分区,然后又进行了一次HASH分区,所以分区的数量应该为(3×2=)6个,这通过查看物理磁盘上的文件也可以得到证实。我们也可以通过使用subpartition语法来显示地指出各个子分区的名字,例如对上述的ts表同样可以这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | createtablets(aint,bdate)engine=innodb partitionbyrange(year(b)) subpartitionbyhash(to_days(b))( partitionp0valueslessthan(1990)( subpartitions0, subpartitions1 ), partitionp1valueslessthan(2000)( subpartitions2, subpartitions3 ), partitionp2valueslessthanmaxvalue( subpartitions4, subpartitions5 ) ); |
子分区的建立需要注意以下几个问题:
1、每个子分区的数量必须相同。
2、要在一个分区表的任何分区上使用subpartition来明确定义任何子分区,就必须定义所有的子分区。
3、每个subpartition子句必须包括子分区的一个名字。
4、子分区的名字必须是唯一的。
子分区可以用于特别大的表,在多个磁盘分别分配数据和索引,用到不错,语句这里就不介绍了,可以MySQL技术内幕-Innodb存储引擎书籍。
四、分区中的NULL值
MySQL数据库允许对NULL值做分区,但是处理的方法与其他数据库可能完全不同。MySQL数据库的分区总是视NULL值小于任何的一个非NULL值,这和MySQL数据库中处理NULL值的ORDER BY操作是一样的。因此对于不同的分区类型,MySQL数据库对于NULL值的处理也是各不相同。
1)对于RANGE分区,如果向分区列插入了NULL值,则MySQL数据库会将该值放入最左边的分区。
1 2 3 4 5 6 7 8 9 | 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 ); |
1 2 3 4 5 6 7 | mysql>insertintot_rangeselect1,1; QueryOK,1rowaffected(0.00sec) Records:1 Duplicates:0 Warnings:0
mysql>insertintot_rangeselect1,NULL; QueryOK,1rowaffected(0.00sec) Records:1 Duplicates:0 Warnings:0 |
1 2 3 4 5 6 7 8 | mysql> select TABLE_ROWS from information_schema.partitions where table_schema=database() and table_name='t_range'\G *************************** 1. row *************************** TABLE_ROWS: 2 *************************** 2. row *************************** TABLE_ROWS: 0 *************************** 3. row *************************** TABLE_ROWS: 0 3 rows in set (0.00 sec) |
可以看到两条数据都放入了p0分区,也就是说range分区下,NULL值会放入最左边的分区中。另外需要注意的是,如果删除p0这个分区,删除的将是小于10的记录,并且还有NULL值的记录,这点非常重要。
2)对于LIST分区,如果向分区列插入了NULL值,则必须显示地指出哪个分区放入NULL值,否则会报错。
1 2 3 4 5 6 7 8 | createtablet_list( aint, bint )engine=innodb partitionbylist(b)( partitionp0valuesin(1,3,5,7,9,NULL), partitionp1valuesin(2,4,6,8,10) ); |
1 2 3 4 5 6 7 | mysql> insert into t_list select 1,null; Query OK, 1 row affected (0.00 sec) Records: 1 Duplicates: 0 Warnings: 0
mysql> insert into t_list select 1,null; Query OK, 1 row affected (0.00 sec) Records: 1 Duplicates: 0 Warnings: 0 |
1 2 3 4 5 6 | mysql>selectTABLE_ROWSfrominformation_schema.partitionswheretable_schema=database()andtable_name='t_list'\G ***************************1.row*************************** TABLE_ROWS:2 ***************************2.row*************************** TABLE_ROWS:0 2rowsinset(0.00sec) |
3)对于HASH和KEY分区,对于NULL值的处理方法和RANGE分区、LIST分区不一样。任何分区函数都会将含有NULL值的记录返回为0。
1 2 3 4 5 6 | create table t_hash( a int, b int ) engine=innodb partition by hash(b) partitions 2; |
1 2 3 4 5 6 7 | mysql>insertintot_hashselect1,0; QueryOK,1rowaffected(0.01sec) Records:1 Duplicates:0 Warnings:0
mysql>insertintot_hashselect1,NULL; QueryOK,1rowaffected(0.00sec) Records:1 Duplicates:0 Warnings:0 |
1 2 3 4 5 6 | mysql> select TABLE_ROWS from information_schema.partitions where table_schema=database() and table_name='t_hash'\G *************************** 1. row *************************** TABLE_ROWS: 2 *************************** 2. row *************************** TABLE_ROWS: 0 2 rows in set (0.01 sec) |
五、分区和性能
分区真的会加快数据库的查询吗?实际上可能根本感觉不到查询速度的提升,甚至会发现查询速度急剧下降,因此在合理使用分区之前,必须了解分区的使用环境。
数据库的应用分为两类:一类是OLTP(在线事务处理),如Blog、电子商务、网络游戏等;另一类是OLAP(在线分析处理),如数据仓库、数据集市。对于OLAP的应用,分区的确是可以很好地提高查询的性能,因为OLAP应用大多数查询需要频繁地扫描一张很大的表。假设有一张1亿行的表,其中有一个时间戳属性列。用户的查询需要从这张表中获取一年的数据。如果按时间戳进行分区,则只需要扫描相应的分区即可。这就是前面介绍的分区修剪技术。
对于OLTP的应用,分区应该非常小心。在这种应用下,通常不可能会获取一张大表10%的数据,大部分都是通过索引返回几条记录即可。而根据B+树索引的原理可知,对于一张大表,一般的B+树需要2~3次的磁盘IO。因此B+树可以很好地完成操作,不需要分区的帮助,并且设计不好的分区会带来严重的性能问题。
如很多开发团队会认为含有1000w行的表是一张非常巨大的表,所以他们往往会选择采用分区,如对主键做10个HASH的分区,这样每个分区就只有100w的数据了,因此查询应该变得更快了。如select * from table where pk=@pk。但是有没有考虑过这样一种情况:100w和1000w行的数据本身构成的B+树的层次都是一样的,可能都是2~3层。那么上述走主键分区的索引并不会带来性能的提高。好的,如果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。
由以上结论可以看出,对于在OLTP场景中使用分区一定要特别小心了。
六、分区键必须包含在主键字段
MySQL的分区字段,必须包含在主键字段内。在对表进行分区时,如果分区字段没有包含在主键字段内,如表user的主键为id,分区字段为crtTime ,要想根据时间按月分区,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | CREATETABLE`TradeOrderDetails`( `id`bigint(20)NOTNULLAUTO_INCREMENT, `tradeNo`varchar(20)NOTNULL, `crtTime`datetimeNOTNULL, `updTime`datetimeDEFAULTNULL, PRIMARYKEY(`id`) )ENGINE=InnoDBDEFAULTCHARSET=utf8 partitionbyrange(to_days(crtTime))( partitionp201705valueslessthan(to_days('2017-06-01')), partitionp201706valueslessthan(to_days('2017-07-01')), partitionp201707valueslessthan(to_days('2017-08-01')), partitionp201708valueslessthan(to_days('2017-09-01')), partitionp201709valueslessthan(to_days('2017-10-01')), partitionp201710valueslessthan(to_days('2017-11-01')), partitionp201711valueslessthan(to_days('2017-12-01')), partitionp201712valueslessthan(to_days('2018-01-01')), partitionp201801valueslessthan(to_days('2018-02-01')), partitionp2018valueslessthanmaxvalue); |
错误提示:#1503
A PRIMARY KEY MUST INCLUDE ALL COLUMNS IN THE TABLE’S PARTITIONING FUNCTIONMySQL
主键的限制,每一个分区表中的公式中的列,必须在primary key/unique key中包括。分区字段必须包含在主键字段内,至于为什么MySQL会这样考虑,CSDN的斑竹是这么解释的:为了确保主键的效率,否则同一主键区的东西一个在A分区,一个在B分区,显然会比较麻烦。
下面讨论解决办法,毕竟在一张表里,日期做主键的还是不常见。 顺应MySQL的要求,就把分区字段加入到主键中,组成复合主键即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | CREATETABLE`TradeOrderDetails`( `id`bigint(20)NOTNULLAUTO_INCREMENT, `tradeNo`varchar(20)NOTNULL, `crtTime`datetimeNOTNULL, `updTime`datetimeDEFAULTNULL, PRIMARYKEY(`id`,`crtTime`) )ENGINE=InnoDBDEFAULTCHARSET=utf8 partitionbyrange(to_days(crtTime))( partitionp201705valueslessthan(to_days('2017-06-01')), partitionp201706valueslessthan(to_days('2017-07-01')), partitionp201707valueslessthan(to_days('2017-08-01')), partitionp201708valueslessthan(to_days('2017-09-01')), partitionp201709valueslessthan(to_days('2017-10-01')), partitionp201710valueslessthan(to_days('2017-11-01')), partitionp201711valueslessthan(to_days('2017-12-01')), partitionp201712valueslessthan(to_days('2018-01-01')), partitionp201801valueslessthan(to_days('2018-02-01')), partitionp2018valueslessthanmaxvalue); |
七、MySQL 5.7对分区的改进
http://mysqlserverteam.com/innodb-native-partitioning-early-access
上面是MySQL开发团队写的关于InnoDB Native Partitioning的文章。文章中大概讲的内容是,在MySQL 5.6里面,分区的信息是在MySQL Server层维护的(在.par文件里面),InnoDB引擎层是不知道有分区这个概念的,InnoDB引擎层把每一个分区都当成一张普通的InnoDB表。在打开一个分区表时,会打开很多个分区,打开这些分区表就相当于打开了同等数量的InnoDB表,这需要更多内存存放InnoDB表的元数据和各种与ibd文件打开相关的各种cache与handler的信息。在MySQL 5.7里面,InnoDB引入了Native Partitioning,它把分区的信息从Server层移到了InnoDB层,打开一个分区表和打开一个InnoDB表的内存开销基本是一样的。
If we compare the amount of memory used when opening a single instance of this table, first using the old generic non-native partitioning, and then with InnoDB Native Partitioning we see the following:
One open instance of the table takes 49% less memory (111MB vs 218MB) with the current state of Native Partitioning support. With ten open instances of the table, we take up 90% less memory (113MB vs 1166MB)!
分区表在线DDL的支持:https://dev.mysql.com/doc/refman/5.7/en/online-ddl-partitioning.html
原文链接 http://www.ywnds.com/?p=7226