分区的过程是将一个表或索引分解为多个更小、更可管理的部分。就访问数据库的应用而言,从逻辑上讲,只有一个表或索引,但是在物理上这个表或索引可能由数十个物理分区组成。每个分区都是独立的对象,可以独自处理,也可以作为一个更大的对象的一部分进行处理。
MySQL数据库支持的分区类型为水平分区,并不支持垂直分区。此外,MySQL数据库的分区是局部分区索引,一个分区中既存放了数据又存放了索引。
可以通过以下命令查看当前数据库是否启用了分区功能:
mysql> SHOW VARIABLES LIKE '%partition%';
当前MySQL数据库支持以下几种类型的分区
分区 | 分区标准 |
RANGE分区 | 行数据基于属于一个给定连续区间的列值被放入分区 |
LIST分区 | 和RANGE分区类型一样,只是LIST分区面向的是离散的值 |
HASH分区 | 根据用户自定义的表达式的返回值来进行分区 |
KEY分区 | 根据MySQL数据库提供的哈希函数进行分区 |
RANGE分区
例:创建一个表id小于10时,数据放入p0分区大于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文件组成,如下:
-rw-rw---- 1 mysql mysql 580 Nov 1 17:48 t.frm
-rw-rw---- 1 mysql mysql 36 Nov 1 17:48 t.par
-rw-rw---- 1 mysql mysql 64K Nov 1 17:48 t#P#p0.ibd
-rw-rw---- 1 mysql mysql 64K Nov 1 17:48 t#P#p1.ibd
接着插入数据:
mysql> INSERT INTO t SELECT 9;
mysql> INSERT INTO t SELECT 10;
mysql> INSERT INTO t SELECT 15;
查询下PARTITIONS
表
*************************** 1. row ***************************
TABLE_CATALOG: def
TABLE_SCHEMA: test
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: 2022-11-01 17:48:40
UPDATE_TIME: 2022-11-01 17:57:37
CHECK_TIME: NULL
CHECKSUM: NULL
PARTITION_COMMENT:
NODEGROUP: default
TABLESPACE_NAME: NULL
*************************** 2. row ***************************
TABLE_CATALOG: def
TABLE_SCHEMA: test
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: 2022-11-01 17:48:40
UPDATE_TIME: 2022-11-01 17:57:37
CHECK_TIME: NULL
CHECKSUM: NULL
PARTITION_COMMENT:
NODEGROUP: default
TABLESPACE_NAME: NULL
对于表t,由于定义了分区,因此对于插入的值应严格遵守分区的定义,当插入一个不在分区内的值时,MySQL数据库会抛出异常:
mysql> INSERT INTO t SELECT 30;
ERROR 1526 (HY000): Table has no partition for value 30
对于上述问题,可以对分区添加一个MAXVALUE值的分区。MAXVALUE可以理解为正无穷,因此所有大于等于20且小于MAXVALUE的值分别放入p2分区:
mysql> ALTER TABLE t ADD PARTITION(partition p2 values less than maxvalue);
在此之后插入记录:
mysql> INSERT INTO t SELECT 30;
Query OK, 1 row affected (0.001 sec)
Records: 1 Duplicates: 0 Warnings: 0
RANGE分区主要用于日期列的分区,如下例:
CREATE TABLE sales (
money INT UNSIGNED NOT NULL, date DATE) 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)
);
mysql> INSERT INTO sales SELECT 100,'2008-01-01';
mysql> INSERT INTO sales SELECT 100,'2008-02-01';
mysql> INSERT INTO sales SELECT 200,'2008-01-02';
mysql> INSERT INTO sales SELECT 100,'2009-03-01';
mysql> INSERT INTO sales SELECT 200,'2010-03-01';
如果要删除2009年的数据,不需要执行DELECT FROM sales WHERE date>='2008-01-01' and date<'2000-01-01'
,只需删除2008年所在的分区即可:
ALTER TABLE sales drop partition p2008;
mysql> EXPLAIN PARTITIONS
SELECT * FROM sales
WHERE date>='2008-01-01' AND date<='2008-12-31'\G;
通过EXPLAIN PARTITION
命令,在上述语句中,SQL优化器只需要去搜索p2008这个分区,而不会去搜索所有的分区——称为Partition Pruning(分区修剪),故查询的速度得到了大幅度的提升。
然后在条件改为:
mysql> EXPLAIN PARTITIONS
SELECT * FROM sales
WHERE date>='2008-01-01' AND date<'2009-01-01'\G;
时优化器会去搜索p2008和p2009两个分区。
再如:
mysql> 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)
);
按照上述分区,优化器不会根据分区进行选择,即使编写的SQL已经符合了分区的要求。
产生这个问题的主要原因是对于RANGE分区的查询,优化器只能对YEAR(),TO_DAYS(),TO_SECONDS(),UNIX_TIMESTAMP()
这类函数进行优化选择。
LIST分区
LIST分区和RANGE分区非常相似,只是分区列的值是离散的,而非连续的。
mysql> CREATE TABLE t (
a INT,
b INT)ENGIN=INNODB
PARTITION BY LIST(b)(
PARTITION p0 VALUES IN (1,3,5,7,9),
PARTITION p1 VALUES IN (0,2,4,6,8)
);
不同于RANGE分区中定义的VALUES LESS THAN
语句,LIST分区使用VALUES IN。因为每个分区的值是离散的,因此只能定义值。
例:
mysql> 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)
);
mysql> INSERT INTO t SELECT 1,1;
mysql> INSERT INTO t SELECT 1,2;
mysql> INSERT INTO t SELECT 1,3;
mysql> INSERT INTO t SELECT 1,4;
mysql> SELECT table_name,partition_name,table_rows
FROM information_schema.PARTITIONS
WHERE table_name='t' AND table_schema=DATABASE()\G;
可得到结果:
*************************** 1. row ***************************
table_name: t
partition_name: p0
table_rows: 2
*************************** 2. row ***************************
table_name: t
partition_name: p1
table_rows: 2
如果插入的值不在分区定义中,MySQL数据库同样会抛出异常:
mysql> INSERT INTO t SELECT 1,10;
其返回:
ERROR 1526 (HY000): Table has no partition for value 10
另外,在用INSERT插入多个行数据的过程中遇到分区未定义的值时,MyISAM和InnoDB存储引擎的处理不同。MyISAM引擎会将之前的行数据都插入,但之后的数据不会被插入。而InnoDB存储引擎将其视为一个事务,因此没有任何数据插入。
HASH分区
HASH分区的目的是将数据均匀分布到预先定义的各个分区中,保证各分区的数据量大致都是一样的。在RANGE和LIST分区中,必须明确指定一个给定的列值或列值集合应该保存到哪个分区中;而在HASH分区中,MySQL自动完成这些工作,用户所要做的只是基于将要进行哈希分区的列值指定一个列值或表达式,以及指定被分区的表将要被分割成的分区数量。
CREATE TABLE table_name(col_name[,colname]...)ENGINE=InnoDB
PARTITION BY HASH (expr)
PARTITONS num;
参数 | 含义 |
table_name | 表名 |
col_name | 字段名 |
expr | 一个返回一个整数的表达式 |
num | 一个非负整数,它表示表将要被分割成分区的数量,如果没有PARTITIONS子句,那么分区的数量将默认为1 |
另外MySQL还支持一种称为LINEAR HASH
的分区,它使用一个更加复杂的算法来确定新行插入到已经分区的表中的位置。它的语法与HASH分区语法类似,只是将HASH
改为LINEAR HASH
LINEAR HASH
分区的优点在于,增加、删除、合并和拆分分区将变得更加快捷,这有利于含有大量数据的表。它的缺点在于,与使用HASH
分区得到的数据分布相比,各个分区间数据的分布可能不大均衡。
KEY分区
KEY分区和HASH分区相似,不同之处在于HASH分区使用用户定义的函数进行分区,KEY分区使用MySQL数据库提供的函数进行分区。
在KEY分区中使用关键字LINEAR和HASH分区中使用具有同样的效果,分区的编号是通过2的幂算法得到的,而不是通过模数算法。
COLUMNS分区
前面介绍的RANGE、LIST、HASH和KEY这四种分区中,分区的条件是:数据必须是整形(interger),如果不是整形,那应该通过函数将其转化为整形,如YEAR()、TO_DAYS()、MONTH()等函数。
COLUMNS分区可以直接使用非整形的数据进行分区。此外,RANGE COLUMNS分区可以对多个列的值进行分区。
COLUMNS分区支持的数据类型
所有整形类型 | 如INT、SMALLINT、TINYINT、BIGINT。FLOAT和DECIMAL不予支持 |
日期类型 | 如DATE和DATETIME.其余的日期类型不予支持 |
字符串类型 | 如CHAR、VARCHAR、BINARY和VARBINARY。BLOB和TEXT类型不予支持 |
例:
类RANGE:
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')
);
类LIST:
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(city)(
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,b,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)
);
子分区
子分区(subpartitioning)是在分区的基础上在进行分区,有时也称这种分区为复合分区(composite partitioning)
例:在RANGE分区下在进行HASH子分区:
mysql> 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
);
也可以这样:
mysql> CREATE TABLE ts(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来明确定义任何子分区,就必须定义所有子分区
- 每个SUBPARTITION子句必须包括子分区的一个名字
- 子分区的名字必须是唯一的
在表和分区之间交换数据
将表1的某个分区中的数据移动到表2:
ALTER TABLE 表名1 EXCHANGE PARTITION 分区名 WITH TABLE 表名2;
要使用ALTER TABLE...EXCHANGE PARTITION
语句,必须满足下面的条件:
- 要交换的表需和分区表有着相同的表结构,但是表不能含有分区
- 在非分区表中的数据必须在交换的分区定义内
- 被交换的表中不能含有外键,或者其他表含有对该表的外键引用
- 用户除了需要
ALTER、INSERT、CREATE
权限外,还需要DROP
的权限
此外 - 使用该语句时,不会触发交换表和被交换表上的触发器
AUTO_INCREMENT
列将被重置
例:
mysql> CREATE TABLE e (
id INT NOT NULL,
fname VARCHAR(30),
lname VARCHAR(30)
)
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)
);
mysql> INSERT INTO e VALUES(1669,"Jim","Smith"),(337,"Mart","Jones"),(16,"Frank","White"),(2005,"Linda","Black");
mysql> CREATE TABLE e2 LIKE e;
mysql> ALTER TABLE e2 REMOVE PARTITIONING;
此时查看:
mysql> SELECT PARTITION_NAME,TABLE_ROWS
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_NAME='e';
得到结果:
+----------------+------------+
| PARTITION_NAME | TABLE_ROWS |
+----------------+------------+
| p0 | 1 |
| p1 | 0 |
| p2 | 0 |
| p3 | 3 |
+----------------+------------+
4 rows in set (0.010 sec)
执行
mysql> ALTER TABLE e EXCHANGE PARTITION p0 WITH TABLE e2;
此时查看:
mysql> SELECT PARTITION_NAME,TABLE_ROWS
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_NAME='e';
得到结果:
+----------------+------------+
| PARTITION_NAME | TABLE_ROWS |
+----------------+------------+
| p0 | 0 |
| p1 | 0 |
| p2 | 0 |
| p3 | 3 |
+----------------+------------+
4 rows in set (0.001 sec)
而这时:
mysql> SELECT * FROM e2;
有:
+----+-------+-------+
| id | fname | lname |
+----+-------+-------+
| 16 | Frank | White |
+----+-------+-------+
p0的数据被移动到了e2