Mysql笔记-分区

这篇文章整理了mysql数据库中有关分区的知识(RANGE、LIST、HASH、KEY)。参考资料《Mysql技术手册》、《MySQL技术内幕InnoDB存储引擎》和《高性能MySQL》。

本文结构如下,首先介绍四种分区,然后介绍子分区以及分区中对null值的处理。

RANGE分区

按照RANGE分区的表是通过如下一种方式进行分区的,每个分区包含那些分区表达式的值位于一个给定的连续区间内的行。这些区间要连续且不能相互重叠,使用VALUES LESS THAN操作符来进行定义。

例子:

mysql> createtable rg(
    -> date date,
    -> body text
    -> )engine=innodb
    -> partition by range (year(date))(
    -> partition p2014 values lessthan(2014),
    -> partition p2015 values lessthan(2015)
    -> );
Query OK, 0 rowsaffected (0.06 sec)
mysql> insert into rgvalues("20131012","happy birthday");
Query OK, 1 row affected (0.00 sec)
mysql> insert into rgvalues("20141012","hello world");
Query OK, 1 row affected (0.00 sec)

我们可以通过information_schema.partitions来查看分区情况

mysql> select * from information_schema.partitions
    -> wheretable_schema=database() and table_name="rg"\G
*************************** 1. row***************************
                TABLE_CATALOG:def
                TABLE_SCHEMA: world
                  TABLE_NAME: rg
              PARTITION_NAME: p2014
           SUBPARTITION_NAME: NULL
  PARTITION_ORDINAL_POSITION: 1
SUBPARTITION_ORDINAL_POSITION: NULL
            PARTITION_METHOD: RANGE
         SUBPARTITION_METHOD: NULL
        PARTITION_EXPRESSION: year(date)
     SUBPARTITION_EXPRESSION: NULL
       PARTITION_DESCRIPTION: 2014
                  TABLE_ROWS: 1
              AVG_ROW_LENGTH: 16384
                 DATA_LENGTH: 16384
             MAX_DATA_LENGTH: NULL
                INDEX_LENGTH: 0
                   DATA_FREE: 0
                 CREATE_TIME: 2016-08-18 18:59:03
                 UPDATE_TIME: 2016-08-18 19:00:26
                   CHECK_TIME: NULL
                    CHECKSUM: NULL
           PARTITION_COMMENT:
                   NODEGROUP: default
             TABLESPACE_NAME: NULL
*************************** 2. row***************************
               TABLE_CATALOG: def
                TABLE_SCHEMA: world
                  TABLE_NAME: rg
              PARTITION_NAME: p2015
           SUBPARTITION_NAME: NULL
  PARTITION_ORDINAL_POSITION: 2
SUBPARTITION_ORDINAL_POSITION: NULL
            PARTITION_METHOD: RANGE
          SUBPARTITION_METHOD: NULL
        PARTITION_EXPRESSION: year(date)
     SUBPARTITION_EXPRESSION: NULL
       PARTITION_DESCRIPTION: 2015
                  TABLE_ROWS: 1
              AVG_ROW_LENGTH: 16384
                 DATA_LENGTH: 16384
             MAX_DATA_LENGTH: NULL
                INDEX_LENGTH: 0
                   DATA_FREE: 0
                 CREATE_TIME: 2016-08-18 18:59:03
                 UPDATE_TIME: 2016-08-18 19:00:48
                  CHECK_TIME: NULL
                     CHECKSUM: NULL
           PARTITION_COMMENT:
                   NODEGROUP: default
             TABLESPACE_NAME: NULL
2 rows in set (0.00 sec)

我们可以看到p2014分区中有一行数据而p2015分区中有两行数据。

现在我们通过explain来看看查询一条数据的情况,

mysql> explain select * from rg wheredate<"20140101"\G
*************************** 1. row***************************
           id: 1
  select_type:SIMPLE
        table:rg
   partitions:p2014
         type:ALL
possible_keys: NULL
          key:NULL
      key_len:NULL
          ref:NULL
         rows: 1
     filtered:100.00
        Extra:Using where
1 row in set, 1 warning (0.00 sec)

我们可以看到只用到了p2014分区,而没有访问p2015分区。我们称之为-partition pruning(分区修剪)。

在这里我又做了如下实验,

mysql> insert into rgvalues("20151012","hello world");
ERROR 1526 (HY000): Table has nopartition for value 2015

我们发现当插入一个不在分区中的定义的值时,会抛出一个异常。如上。

对于这种情况,我们可以添加一个maxvalue值的分区。Maxvalue可以理解为无穷大。

mysql> alter table rg
    -> add partition (
    -> partition pmax values less thanmaxvalue);
Query OK, 0 rows affected (0.08 sec)
Records: 0  Duplicates: 0 Warnings: 0
mysql> insert into rgvalues("20151012","hello world");
Query OK, 1 row affected (0.02sec)

在此,我们讨论下这样创建表分区的好处。

我们如果想删除2014年以前的数据,我们不必再执行

delete from rg wheredate>=”20130101”and date <=”20140101”;

我们只需要删除p2004分区即可。

mysql> alter table rg
    -> drop partition p2014
    -> ;
Query OK, 0 rows affected (0.05sec)
Records: 0  Duplicates: 0 Warnings: 0

List分区

MySQL中的LIST分区在很多方面类似于RANGE分区。和按照RANGE分区一样,每个分区必须明确定义。它们的主要区别在于,LIST分区中每个分区的定义和选择是基于某列的值从属于一个值列表集中的一个值,而RANGE分区是从属于一个连续区间值的集合。LIST分区通过使用“PARTITION BY LIST(expr)”来实现,其中“expr” 是某列值或一个基于某个列值、并返回一个整数值的表达式,然后通过“VALUES IN (value_list)”的方式来定义每个分区,其中“value_list”是一个通过逗号分隔的整数列表。

例子来自mysql手册

假定有20个音像店,分布在4个有经销权的地区,如下表所示:

地区

商店ID 号

北区

3, 5, 6, 9, 17

东区

1, 2, 10, 11, 19, 20

西区

4, 12, 13, 14, 18

中心区

7, 8, 15, 16

要按照属于同一个地区商店的行保存在同一个分区中的方式来分割表,可以使用下面的“CREATE TABLE”语句:

CREATE TABLE employees (
    id INT NOTNULL,
    fnameVARCHAR(30),
    lnameVARCHAR(30),
    hired DATE NOTNULL DEFAULT '1970-01-01',
    separated DATENOT NULL DEFAULT '9999-12-31',
    job_code INT,
    store_id INT
)
PARTITION BY LIST(store_id)
    PARTITIONpNorth VALUES IN (3,5,6,9,17),
    PARTITIONpEast VALUES IN (1,2,10,11,19,20),
    PARTITIONpWest VALUES IN (4,12,13,14,18),
    PARTITIONpCentral VALUES IN (7,8,15,16)
);

这使得在表中增加或删除指定地区的雇员记录变得容易起来。例如,假定西区的所有音像店都卖给了其他公司。那么与在西区音像店工作雇员相关的所有记录(行)可以使用查询“ALTER TABLE employees DROP PARTITION pWest;”来进行删除,它与具有同样作用的DELETE (删除)查询“DELETE query DELETE FROMemployees WHERE store_id IN (4,12,13,14,18);”比起来,要有效得多。

要点:如果试图插入列值(或分区表达式的返回值)不在分区值列表中的一行时,那么“INSERT”查询将失败并报错。例如,假定LIST分区的采用上面的方案,下面的查询将失败:

INSERT INTO employees VALUES
    (224,'Linus', 'Torvalds', '2002-05-01', '2004-10-12', 42, 21);

HASH分区

HASH分区主要用来确保数据在预先确定数目的分区中平均分布。在RANGE和LIST分区中,必须明确指定一个给定的列值或列值集合应该保存在哪个分区中;而在HASH分区中,MySQL 自动完成这些工作,你所要做的只是基于将要被哈希的列值指定一个列值或表达式,以及指定被分区的表将要被分割成的分区数量。

要使用HASH分区来分割一个表,要在CREATE TABLE 语句上添加一个“PARTITION BY HASH (expr)”子句,其中“expr”是一个返回一个整数的表达式。它可以仅仅是字段类型为MySQL 整型的一列的名字。此外,你很可能需要在后面再添加一个“PARTITIONS num”子句,其中num 是一个非负的整数,它表示表将要被分割成分区的数量。

例如,下面的语句创建了一个使用基于“store_id”列进行 哈希处理的表,该表被分成了4个分区:

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
)
PARTITION BY HASH(store_id)
PARTITIONS 4;

LINEAR HASH分区

MySQL还支持线性哈希功能,它与常规哈希的区别在于,线性哈希功能使用的一个线性的2的幂(powers-of-two)运算法则,而常规 哈希使用的是求哈希函数值的模数。

线性哈希分区和常规哈希分区在语法上的唯一区别在于,在“PARTITION BY” 子句中添加“LINEAR”关键字,如下面所示:

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
)
PARTITION BY LINEAR HASH(YEAR(hired))
PARTITIONS 4;

假设一个表达式expr, 当使用线性哈希功能时,记录将要保存到的分区是num 个分区中的分区N,其中N是根据下面的算法得到:

1.   找到下一个大于num.的、2的幂,我们把这个值称为V ,它可以通过下面的公式得到:

2.V=POWER(2,CEILING(LOG(2,NUM))

(例如,假定num是13。那么LOG(2,13)就是3.7004397181411。 CEILING(3.7004397181411)就是4,则V = POWER(2,4), 即等于16)。

3.   设置 N = F(column_list) & (V- 1).

4.   当 N >= num:

·        设置 V = CEIL(V / 2)

·        设置 N = N & (V - 1)

例如,假设表t1,使用线性哈希分区且有4个分区,是通过下面的语句创建的:

CREATE TABLE t1 (col1 INT, col2 CHAR(5), col3 DATE)
    PARTITION BY LINEAR HASH( YEAR(col3) )
    PARTITIONS 6;

现在假设要插入两行记录到表t1中,其中一条记录col3列值为'2003-04-14',另一条记录col3列值为'1998-10-19'。第一条记录将要保存到的分区确定如下:

V = POWER(2, CEILING(LOG(2,7))) = 8
N = YEAR('2003-04-14') & (8 - 1)
   = 2003 & 7
   = 3
 
(3 >= 6 为假(FALSE): 记录将被保存到#3号分区中)

第二条记录将要保存到的分区序号计算如下:

V = 8
N = YEAR('1998-10-19') & (8-1)
  = 1998 & 7
  = 6
 
(6 >= 4 为真(TRUE): 还需要附加的步骤)
 
N = 6 & CEILING(5 / 2)
  = 6 & 3
  = 2
 
(2 >= 4 为假(FALSE): 记录将被保存到#2分区中)

按照线性哈希分区的优点在于增加、删除、合并和拆分分区将变得更加快捷,有利于处理含有极其大量(1000吉)数据的表。它的缺点在于,与使用常规HASH分区得到的数据分布相比,各个分区间数据的分布不大可能均衡。

KEY分区

按照KEY进行分区类似于按照HASH分区,除了HASH分区使用的用户定义的表达式,而KEY分区的 哈希函数是由MySQL 服务器提供。MySQL 簇(Cluster)使用函数MD5()来实现KEY分区;对于使用其他存储引擎的表,服务器使用其自己内部的 哈希函数,这些函数是基于与PASSWORD()一样的运算法则。

“CREATE TABLE ... PARTITIONBY KEY”的语法规则类似于创建一个通过HASH分区的表的规则。它们唯一的区别在于使用的关键字是KEY而不是HASH,并且KEY分区只采用一个或多个列名的一个列表。

通过线性KEY分割一个表也是可能的。下面是一个简单的例子:
CREATE TABLE tk (
    col1 INT NOT NULL,
    col2 CHAR(5),
    col3 DATE
) 
PARTITION BY LINEAR KEY (col1)
PARTITIONS 3;

在KEY分区中使用关键字LINEAR和在HASH分区中使用具有同样的作用,分区的编号是通过2的幂(powers-of-two)算法得到,而不是通过模数算法。

子分区

子分区是分区表中每个分区的再次分割。例如,考虑下面的CREATETABLE 语句:

CREATE TABLE ts (id INT, purchased DATE)
    PARTITION BY RANGE(YEAR(purchased))
    SUBPARTITION BY HASH(TO_DAYS(purchased))
    SUBPARTITIONS 2
    (
        PARTITION p0 VALUES LESS THAN (1990),
        PARTITION p1 VALUES LESS THAN (2000),
        PARTITION p2 VALUES LESS THAN MAXVALUE
    );

表ts 有3个RANGE分区。这3个分区中的每一个分区——p0, p1, 和 p2 ——又被进一步分成了2个子分区。实际上,整个表被分成了3 * 2 = 6个分区。但是,由于PARTITION BY RANGE子句的作用,这些分区的头2个只保存“purchased”列中值小于1990的那些记录。

在MySQL 5.1中,对于已经通过RANGE或LIST分区了的表再进行子分区是可能的。子分区既可以使用HASH希分区,也可以使用KEY分区。这也被称为复合分区composite partitioning)。

为了对个别的子分区指定选项,使用SUBPARTITION 子句来明确定义子分区也是可能的。例如,创建在前面例子中给出的同一个表的、一个更加详细的方式如下:

CREATE TABLE ts (id INT, purchased DATE)
    PARTITION BY RANGE(YEAR(purchased))
    SUBPARTITION BY HASH(TO_DAYS(purchased))
    (
        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
        )
    );

几点要注意的语法项:

l  每个分区必须有相同数量的子分区。

l  如果在一个分区表上的任何分区上使用SUBPARTITION 来明确定义任何子分区,那么就必须定义所有的子分区。

l  每个SUBPARTITION子句必须包括 (至少)子分区的一个名字。否则,你可能要对该子分区设置任何你所需要的选项,或者允许该子分区对那些选项采用其默认的设置。

l  在每个分区内,子分区的名字必须是唯一的,但是在整个表中,没有必要保持唯一。例如,下面的CREATE TABLE 语句是有效的:

CREATE TABLE ts (id INT, purchased DATE)
PARTITION BY RANGE(YEAR(purchased))
SUBPARTITION BY HASH(TO_DAYS(purchased))
(
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
)
);

子分区可以用于特别大的表,在多个磁盘间分配数据和索引。假设有6个磁盘,分别为/disk0,/disk1, /disk2等。现在考虑下面的例子:

CREATE TABLE ts (id INT, purchased DATE)
    PARTITION BY RANGE(YEAR(purchased))
    SUBPARTITION BY HASH(TO_DAYS(purchased))
    (
        PARTITION p0 VALUES LESS THAN (1990)
        (
            SUBPARTITION s0 
                DATA DIRECTORY = '/disk0/data' 
                INDEX DIRECTORY = '/disk0/idx',
            SUBPARTITION s1 
                DATA DIRECTORY = '/disk1/data' 
                INDEX DIRECTORY = '/disk1/idx'
        ),
        PARTITION p1 VALUES LESS THAN (2000)
        (
            SUBPARTITION s0 
                DATA DIRECTORY = '/disk2/data' 
                INDEX DIRECTORY = '/disk2/idx',
            SUBPARTITION s1 
                DATA DIRECTORY = '/disk3/data' 
                INDEX DIRECTORY = '/disk3/idx'
        ),
        PARTITION p2 VALUES LESS THAN MAXVALUE
        (
            SUBPARTITION s0 
                DATA DIRECTORY = '/disk4/data' 
                INDEX DIRECTORY = '/disk4/idx',
            SUBPARTITION s1 
                DATA DIRECTORY = '/disk5/data' 
                INDEX DIRECTORY = '/disk5/idx'
        )
    );

在这个例子中,每个RANGE分区的数据和索引都使用一个单独的磁盘。还可能有许多其他的变化;下面是另外一个可能的例子:

CREATE TABLE ts (id INT, purchased DATE)
    PARTITION BY RANGE(YEAR(purchased))
    SUBPARTITION BY HASH(TO_DAYS(purchased))
    (
        PARTITION p0 VALUES LESS THAN (1990)
        (
            SUBPARTITION s0a 
                DATA DIRECTORY = '/disk0' 
                INDEX DIRECTORY = '/disk1',
            SUBPARTITION s0b 
                DATA DIRECTORY = '/disk2' 
                INDEX DIRECTORY = '/disk3'
        ),
        PARTITION p1 VALUES LESS THAN (2000)
        (
            SUBPARTITION s1a 
                DATA DIRECTORY = '/disk4/data' 
                INDEX DIRECTORY = '/disk4/idx',
            SUBPARTITION s1b 
                DATA DIRECTORY = '/disk5/data' 
                INDEX DIRECTORY = '/disk5/idx'
        ),
        PARTITION p2 VALUES LESS THAN MAXVALUE
        (
            SUBPARTITION s2a,
            SUBPARTITION s2b
        )
    );


在这个例子中,存储的分配如下:

购买日期在1990年前的记录占了大量的存储空间,所以把它分为了四个部分进行存储,组成p0分区的两个子分区(s0a 和s0b)的数据和索引都分别用一个单独的磁盘进行存储。换句话说:

o       子分区s0a 的数据保存在磁盘/disk0中。

o       子分区s0a 的索引保存在磁盘/disk1中。

o       子分区s0b 的数据保存在磁盘/disk2中。

o       子分区s0b 的索引保存在磁盘/disk3中。

保存购买日期从1990年到1999年间的记录(分区p1)不需要保存购买日期在1990年之前的记录那么大的存储空间。这些记录分在2个磁盘(/disk4和/disk5)上保存,而不是4个磁盘:

o       属于分区p1的第一个子分区(s1a)的数据和索引保存在磁盘/disk4上 — 其中数据保存在路径/disk4/data下,索引保存在/disk4/idx下。

o       属于分区p1的第二个子分区(s1b)的数据和索引保存在磁盘/disk5上 — 其中数据保存在路径/disk5/data下,索引保存在/disk5/idx下。

·        保存购买日期从2000年到现在的记录(分区p2)不需要前面两个RANGE分区那么大的空间。当前,在默认的位置能够足够保存所有这些记录。

将来,如果从2000年开始后十年购买的数量已经达到了默认的位置不能够提供足够的保存空间时,相应的记录(行)可以通过使用“ALTER TABLE ... REORGANIZE PARTITION”语句移动到其他的位置。

 MySQL分区处理NULL值的方式

MySQL 中的分区在禁止空值(NULL)上没有进行处理,无论它是一个列值还是一个用户定义表达式的值。一般而言,在这种情况下MySQL 把NULL视为0。如果你希望回避这种做法,你应该在设计表时不允许空值;最可能的方法是,通过声明列“NOT NULL”来实现这一点。

在本节中,我们提供了一些例子,来说明当决定一个行应该保存到哪个分区时,MySQL 是如何处理NULL值的。

如果插入一行到按照RANGE或LIST分区的表,该行用来确定分区的列值为NULL,分区将把该NULL值视为0。例如,考虑下面的两个表,表的创建和插入记录如下:

mysql> CREATE TABLE tnlist (
    ->     id INT,
    ->     name VARCHAR(5)
    -> )
    -> PARTITION BY LIST(id) (
    ->     PARTITION p1 VALUES IN (0),
    ->     PARTITION p2 VALUES IN (1)
    -> );
Query OK, 0 rows affected (0.09 sec)
 
mysql> CREATE TABLE tnrange (
    ->     id INT,
    ->     name VARCHAR(5)
    -> )
    -> PARTITION BY RANGE(id) (
    ->     PARTITION p1 VALUES LESS THAN (1),
    ->     PARTITION p2 VALUES LESS THAN MAXVALUE
    -> );
Query OK, 0 rows affected (0.09 sec)
 
mysql> INSERT INTO tnlist VALUES (NULL, 'bob');
Query OK, 1 row affected (0.00 sec)
 
mysql> INSERT INTO tnrange VALUES (NULL, 'jim');
Query OK, 1 row affected (0.00 sec)
 
mysql> SELECT * FROM tnlist;
+------+------+
| id   | name |
+------+------+
| NULL | bob  |
+------+------+
1 row in set (0.00 sec)
 
mysql> SELECT * FROM tnrange;
+------+------+
| id   | name |
+------+------+
| NULL | jim  |
+------+------+
1 row in set (0.00 sec)

在两个表中,id列没有声明为“NOT NULL”,这意味着它们允许Null值。可以通过删除这些分区,然后重新运行SELECT 语句,来验证这些行被保存在每个表的p1分区中:

mysql> ALTER TABLE tnlist DROP PARTITION p1;
Query OK, 0 rows affected (0.16 sec)
 
mysql> ALTER TABLE tnrange DROP PARTITION p1;
Query OK, 0 rows affected (0.16 sec)
 
mysql> SELECT * FROM tnlist;
Empty set (0.00 sec)
 
mysql> SELECT * FROM tnrange;
Empty set (0.00 sec)

在按HASH和KEY分区的情况下,任何产生NULL值的表达式都视同好像它的返回值为0。我们可以通过先创建一个按HASH分区的表,然后插入一个包含有适当值的记录,再检查对文件系统的作用,来验证这一点。假定有使用下面的语句在测试数据库中创建了一个表tnhash:

CREATE TABLE tnhash (
    id INT,
    name VARCHAR(5)
)
PARTITION BY HASH(id)
PARTITIONS 2;

假如Linux 上的MySQL 的一个RPM安装,这个语句在目录/var/lib/mysql/test下创建了两个.MYD文件,这两个文件可以在bash shell中查看,结果如下:

/var/lib/mysql/test> ls *.MYD -l
-rw-rw----  1 mysql mysql 0 2005-11-04 18:41 tnhash_p0.MYD
-rw-rw----  1 mysql mysql 0 2005-11-04 18:41 tnhash_p1.MYD

注意:每个文件的大小为0字节。现在在表tnhash 中插入一行id列值为NULL的行,然后验证该行已经被插入:

mysql> INSERT INTO tnhash VALUES (NULL, 'sam');
Query OK, 1 row affected (0.00 sec)
 
mysql> SELECT * FROM tnhash;
+------+------+
| id   | name |
+------+------+
| NULL | sam  |
+------+------+
1 row in set (0.01 sec)

回想一下,对于任意的整数N,NULLMOD N 的值总是等于NULL。这个结果在确定正确的分区方面被认为是0。回到系统shell(仍然假定bash用于这个目的) ,通过再次列出数据文件,可以看出值被成功地插入到第一个分区(默认名称为p0)中:

var/lib/mysql/test> ls *.MYD -l
-rw-rw----  1 mysql mysql 20 2005-11-04 18:44 tnhash_p0.MYD
-rw-rw----  1 mysql mysql  0 2005-11-04 18:41 tnhash_p1.MYD
可以看出INSERT语句只修改了文件tnhash_p0.MYD,它在磁盘上的尺寸增加了,而没有影响其他的文件。

假定有下面的一个表:

CREATE TABLE tndate (
    id INT,
    dt DATE
)
PARTITION BY RANGE( YEAR(dt) ) (
    PARTITION p0 VALUES LESS THAN (1990),
    PARTITION p1 VALUES LESS THAN (2000),
    PARTITION p2 VALUES LESS THAN MAXVALUE
);


像其他的MySQL函数一样,YEAR(NULL)返回NULL值。一个dt列值为NULL的行,其分区表达式的计算结果被视为0,该行被插入到分区p0中。 

你可能感兴趣的:(Mysql学习笔记)