MySQL分区表的类型及原理

电子说

1.3w人已加入

描述

分区是将一个表的数据按照某种方式,比如按照月、天或者其他方式,分成多个较小的、更容易管理的部分,也就是物理存储根据一定规则放在不同文件中,但是逻辑上所有的数据仍在一个表中。如下图所示:

MySQL

MySQL实现分区表的方式是对底层表的封装,意味着索引也是按照分区的子表定义的,没有全局索引。这和Oracle不同,在Oracle中可以更加灵活的定义索引和表是否进行分区。

分区表的实现原理

分区表由多个相关的底层表实现,这些底层表也是由句柄对象表示,因此也可以直接访问各个分区。存储引擎管理分区的各个底层表和管理普通表一样,所有的底层表都必须使用相同的存储引擎,分区表的索引只是在各个底层表各自加上一个完全相同的索引。从存储引擎的角度来看,底层表和一个普通表没有任何不同,存储引擎也无需知道这是一个普通表还是一个分区表的一部分。

那么在分区表上的操作是怎样进行的呢?其实常规的CRUD操作以及返回结果和普通表没有任何区别。具体分区层实现是先打开并锁定所有底层表,优化器先判断是否可以过滤部分分区,然后调用对应的存储引擎接口访问各个分区的数据进行相应的操作。

分区表的分区类型

分区表的类型主要包括RANGE、LIST、HASH、KEY四种,另外还有一种COLUMNS分区类型,因不经常用,本文不做介绍。

  • RANGE分区

给定一个连续的区间范围(区间要求连续并且不能重叠),某个字段的值满足这个范围就会被分配到该分区。适用于字段的值是连续的字段,比如日期范围, 连续的数字等。

示例代码:

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 NOT NULL,
    store_id INT NOT NULL
)
PARTITION BY RANGE (YEAR(separated))
 (
    PARTITION p0 VALUES LESS THAN (1991),
    PARTITION p1 VALUES LESS THAN (1996),
    PARTITION p2 VALUES LESS THAN (2001),
    PARTITION p3 VALUES LESS THAN MAXVALUE
)

该示例中根据每个员工离开公司的年份进行划分,对于1991年之前离职员工存储在分区p0中,1991年至1995年离职的人存储在分区p1,1996年至2000年离职的人存储在分区p2中。

如果没有创建LESS THAN MAXVALUE分区,那么可能存在插入超过2000年离职的人的数据丢失不存储,因此RANGE分区为了防止丢数据会加入该分区。MAXVALUE表示一个始终大于最大可能整数值的整数值,因此2001年以后离开的所有人存储在分区p3中。

若设置了 LESS THAN MAXVALUE分区,添加新分区时需要重新分区,此时存储在LESS THAN MAXVALUE分区中的数据会根据重新分区规则进行数据重新分配,如下所示:

ALTER TABLE employees  BY RANGE (YEAR(separated))
(
  PARTITION p0 VALUES LESS THAN (1991),
  PARTITION p1 VALUES LESS THAN (1996),
  PARTITION p2 VALUES LESS THAN (2001),
  PARTITION p3 VALUES LESS THAN (2006),
  PARTITION p4 VALUES LESS THAN MAXVALUE
);

对于日期类型的字段,也可以使用UNIX_TIMESTAMP()函数,根据TIMESTAMP列的值按RANGE对表进行分区,如下例所示:

CREATE TABLE quarterly_report_status (
    report_id INT NOT NULL,
    report_status VARCHAR(20) NOT NULL,
    report_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
PARTITION BY RANGE ( UNIX_TIMESTAMP(report_updated) ) (
    PARTITION p0 VALUES LESS THAN ( UNIX_TIMESTAMP('2020-01-01 00:00:00') ),
    PARTITION p1 VALUES LESS THAN ( UNIX_TIMESTAMP('2020-04-01 00:00:00') ),
    PARTITION p2 VALUES LESS THAN ( UNIX_TIMESTAMP('2020-07-01 00:00:00') ),
    PARTITION p3 VALUES LESS THAN (MAXVALUE)
);
  • LIST分区

和RANGE分区方式相似, LIST分区是基于列值匹配一个离散值集合中的某个值来进行选择分区。LIST分区没有类似RANGE分区中“VALUES LESS THAN MAXVALUE”包含其他值在内的定义,其要匹配的任何值都必须在值列表中找到,如果不在列表中的数据插入表中操作会失败。

示例代码:

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 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)
 );
  • HASH分区

基于用户定义表达式的返回值来进行分区,该表达式使用将要插入到表中的这些行的列值进行计算。

MySQL支持两种HASH分区,常规HASH(HASH)分区和线性HASH(LINEARHASH) 分区。常规HASH分区使用取模算法,对应一个表达式expr可以计算出它被保存到哪个分区中,N = MOD(expr, num)。线性HASH分区使用一个线性的2的幂运算法则,V = POWER(2, CEILING(LOG(2,num)))。

常规HASH示例代码:

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( YEAR(hired) )
PARTITIONS 4;

常规HASH分区非常简便,使用HASH函数值的模数,可以让数据平均的分布到每一个分区,但是由于分区在创建表的时候已经固定了,如果新增或者收缩分区的数据迁移比较大。

因此MySQL还支持线性HASH分区,线性HASH分区采用线性二乘幂算法,可以增加分区维护(增加、删除、合并、拆分分区)时数据迁移和处理速度,但是也会存在各个分区之间数据的分布不太均衡的情况。

语法上线性HASH分区和HASH分区之间的唯一区别是在PARTITION BY子句中添加了LINEAR关键字。如下所示:

线性HASH示例代码:

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 6;

线性HASH分区的计算原理及步骤如下:

  1. V = POWER(2,CEILING(LOG(2, num))),其中:
  • num为分区的数量。
  • LOG(2, num) 代表计算num以2为底的对数。
  • CEILING() 代表向上取整。
  • POWER(2, x) 代表取2的x幂次。

如果num的值是2的幂数,那么这个表达式计算出来的结果不变。

假设num为13。则LOG(2,13)为3.7004397181411。CEILING(3.7004397181411)为4,而V = POWER(2,4)为16。

  1. Set N =F(column_list) & (V - 1)。
  • N代表计算出来的数据所在分区编号。
  • F代表对分区键进行的操作,该操作返回一个整数值。
  • &代表位与运算。

当num是2的倍数时由于V计算出来的结果不变,这个时候线性HASH算法的计算结果F(column_list)&(V-1)=MOD(F(column_list)/num)和常规HASH取模算出的结果是一致的。

  1. While N>= num:
    Set V = V / 2
    
     Set N = N & (V - 1)
    

特别的,如果步骤2中计算出来的N比总分区数量num大,则需要执行本步骤操作,直到N

接下来用一个示例解释下线性HASH算法找具体分区号的实现原理。创建一个有六个分区的线性HASH分区表,如下所示:

CREATE TABLE t1(
   Col1  INT ,
   Col2  CHAR(5) ,
   Col3  DATE 
)
PARTITION BY LINEAR HASH( YEAR(col3) )
PARTITIONS 6;

现在给该表中插入两条记录,Col3的值分别为'2003-04-14'和'1998-10-19',其中上文中的F()在这里对应YEAR(),那么根据算法原理可以确定第一个分区号如下所示:

V = POWER(2, CEILING(LOG(2,6) )) = 8

N = YEAR('2003-04-14')& (8 - 1)

= 2003 & 7

= 3

判断3>=6不成立,所以存储在第3号分区,注意这里的3指的是P3,分区号是从P0开始。

当插入的值是'1998-10-19'计算所存储的分区号如下所示:

V = 8

N = YEAR('1998-10-19')& (8 - 1)

= 1998 & 7

= 6

判断6>=6成立,所以需要做下一步运算。

N = 6 & ((8 / 2) -1)

= 6 & 3

= 2

判断2>=6不成立,所以存储在第2号分区,同理这里的2指的是P2分区。

下面是用EXCEL做的一个简单的测试,测算了一下MySQL使用线性HASH分区算法,将分区键值取(1-1048575),分区数量分别为4、5、6、7、8个时,各分区数据数量如图所示:

MySQL

从图中我们可以看到官网说它是“线性”,但是又可能不太平均,是比较严谨的。从算法角度考虑,在分区键值平均分布的前提下,为了各分区数据量尽量平均,线性HASH推荐分区数量尽量为2的幂次,比如2,4,8,16。如果不能保证,则应尽量让LOG(2, num)越接近于某个2的幂次。

常规HASH和线性HASH增加和收缩分区原理是一样的。增加和收缩分区后原来的数据会根据现有的分区数量重新分布。HASH分区不能删除分区,所以不能使用DROP PARTITION操作进行分区删除操作。可以通过ALTER TABLE ... COALESCE PARTITION num合并分区,这里的num是减去的分区数量。可以通过ALTERTABLE ... ADD PARTITION PARTITIONS num来增加分区,这里是num是增加的分区数量。

  • KEY分区

和HASH分区类似,但是KEY分区不允许使用自定义的表达式,需要使用MySQL Server提供的HASH函数。

MYSQL支持两种KEY分区, 常规KEY(KEY)分区和线性KEY(LINEARKEY) 分区,其中计算区别和HASH分区一样。

常规KEY示例代码:

CREATE TABLE tm1 (
    s1 CHAR(32) PRIMARY KEY
)
PARTITION BY KEY(s1)
PARTITIONS 10;

线性KEY示例代码:

CREATE TABLE tk (
    col1 INT NOT NULL,
    col2 CHAR(5),
    col3 DATE
)
PARTITION BY LINEAR KEY (col1)
PARTITIONS 3;

分区键的注意事项

无论采用哪种分区,要么分区表上没有主键/唯一键,要么分区表的主键/唯一键都必须包含分区键,即不能使用主键/唯一键字段之外的其它字段分区。

针对只包含唯一键的表给出示例分析如下:

下面的三个例子是不正确的:

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;


CREATE TABLE t2 (
    col1 INT NOT NULL,
    col2 DATE NOT NULL,
    col3 INT NOT NULL,
    col4 INT NOT NULL,
    UNIQUE KEY (col1),
    UNIQUE KEY (col3)
)
PARTITION BY HASH(col1 + col3)
PARTITIONS 4;


CREATE TABLE t3 (
         col1 INT NOT NULL,
         col2 DATE NOT NULL,
         col3 INT NOT NULL,
         col4 INT NOT NULL,
         UNIQUE KEY (col1, col2),
         UNIQUE KEY (col3)
 )
 PARTITION BY HASH(col1 + col3)
 PARTITIONS 4;

在以上三个示例中的表都有至少一个唯一键,但是在唯一键中不包括分区表达式中使用的所有列。因此在创建的时候就会报错:ERROR 1491 (HY000): A PRIMARY KEY must include all columns in thetable's partitioning function。

以下三个示例是正确的:

CREATE TABLE t1 (
    col1 INT NOT NULL,
    col2 DATE NOT NULL,
    col3 INT NOT NULL,
    col4 INT NOT NULL,
    UNIQUE KEY (col1, col2, col3)
)
PARTITION BY HASH(col3)
PARTITIONS 4;


CREATE TABLE t2 (
    col1 INT NOT NULL,
    col2 DATE NOT NULL,
    col3 INT NOT NULL,
    col4 INT NOT NULL,
    UNIQUE KEY (col1, col3)
)
PARTITION BY HASH(col1 + col3)
PARTITIONS 4;


CREATE TABLE t3 (
      col1 INT NOT NULL,
      col2 DATE NOT NULL,
      col3 INT NOT NULL,
      col4 INT NOT NULL,
      UNIQUE KEY (col1, col2, col3),
      UNIQUE KEY (col3)
 )
PARTITION BY HASH(col3)
PARTITIONS 4;

在以上三个示例中表的分区键都是所属表的所有唯一键包含的字段,因此可以成功创建。

CREATE TABLE t4 (
    col1 INT NOT NULL,
    col2 INT NOT NULL,
    col3 INT NOT NULL,
    col4 INT NOT NULL,
    UNIQUE KEY (col1, col3),
    UNIQUE KEY (col2, col4)
);

在上面这个示例表是没有办法分区的,因为无法在分区键中包含属于两个唯一键的任何列。

关于主键和唯一键是一样的,这里不重复做示例分析,秉承一个原则:主键/唯一键必须包含分区键的所有列。

分区表优缺点

大多数互联网公司都不建议用MySQL分区表,虽然它有不少好处,但是不足之处也同样很多。接下来讲解下对应的优缺点。

优点

  • 分区表对业务透明,只需要维护一个表的数据结构,DML操作和普通表没有区别。
  • DML操作加锁仅影响操作的分区,不会影响未访问分区。
  • 通过TRUNCATE操作可以快速清理特定分区数据,通过DROP操作也可以直接删除掉不需要的分区和对应的数据。
  • 通过大数据量分区能有效降低索引层数,提高查询性能。

缺点:

  • DDL操作需要锁定所有分区,导致所有分区上操作都被阻塞。
  • 当表数据量较小时,分区表和非分区表性能相近,分区表效果有限。
  • 当表数据量较大时,对分区表进行DDL或其他运维操作难度大、风险高。
  • 分区表在行业内使用较少,社区资料有限,存在未知风险多。
  • 当单台服务器性能无法满足时,对分区表进行分拆的成本较高。
  • 当分区表操作不当导致访问所有分区时,会导致严重的性能问题。
  • 使用分库分表可以有效降低运维操作影响,对1亿数据量表做DDL操作需要谨慎评估,而对10万数据量表做DDL操作可以默认其很快完成。
  • 使用分库分表可以有效减小宕机或其他故障影响,将数据分库分表到10套群集上,一套群集发生故障仅影响一部分的业务。
  • MySQL不支持自动分区扩展,需要手动新增分区并进行数据再均衡。

总 结:

对于上亿行或者更大数量的普通表清理,只能采用DELETE的方式,该方式不但性能差,而且无法真正回收空间。分区表可以通过删除分区等方式,对历史数据进行清理的同时数据文件也做了回收,真正释放了空间、而且效率很高。

很多互联网公司不建议用MySQL分区表,那么我个人的见解是:

对于特定场景是可以考虑采用分区表,如历史数据有明确的分区范围,访问不垮分区,极少的变化操作,查询语句逻辑简单,无性能瓶颈。

对于Oracle这些商业数据库,由于商业授权导致横向扩展成本较高,且分区表功能稳定,因此通过硬件扩展和分区来承担大数据量带来的负载。

对于MySQL开源数据库,企业有资源有能力将很多需求迁移到数据库外通过代码逻辑或者其它替代方式实现,因此更追求MySQL使用过程中的简单、稳定、可靠,且通过增加服务器以及分库分表更能有效处理数据量爆炸式增长带来的性能问题。

因此个人不建议大量使用MySQL分区表,尤其是在重要的业务上。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分