背景
目前我们系统的很多表都处于数据量比较大的情况,当mysql单表的数据库过大时,数据库的访问速度会下降,数据量大问题的常见解决方案是"水平切分",而水平拆分的方案是分库分表和分区表。
我们使用阿里云rds for MySQL数据库(就是MySQL5.7版本),使用 polardb for MySQL会更好些(polardb for MySQL 8.0 以上支持自动增加分区)
理论篇
分库分表
把一个很大的库(表)的数据分到几个库(表)中,每个库(表)的结构都相同,但他们可能分布在不同的mysql实例,甚至不同的物理机器上,以达到降低单库(表)数据量,提高访问性能的目的。
分库分表往往是业务层实施的,分库分表后,为了满足某些特定业务功能,往往需要rd修改代码。
分区表
所有数据还在一个表中,但物理存储根据一定的规则放在不同的文件中。这个是mysql支持的功能,业务rd代码无需改动。
分区的目的及分区类型
MySQL在创建表的时候可以通过使用PARTITION BY
子句定义每个分区存放的数据。在执行查询的时候,优化器根据分区定义过滤那些没有我们需要的数据的分区,这样查询就可以无需扫描所有分区,只需要查找包含需要数据的分区即可。
分区的另一个目的是将数据按照一个较粗的粒度分别存放在不同的表中。这样做可以将相关的数据存放在一起,另外,当我们想要一次批量删除整个分区的数据也会变得很方便。
下面简单介绍下四种常见的分区类型:
-
RANGE分区:最为常用,基于属于一个给定连续区间的列值,把多行分配给分区。最常见的是基于时间字段。
-
LIST分区:LIST分区和RANGE分区类似,区别在于LIST是枚举值列表的集合,RANGE是连续的区间值的集合。
-
HASH分区:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含MySQL中有效的、产生非负整数值的任何表达式。
-
KEY分区:类似于按HASH分区,区别在于KEY分区只支持计算一列或多列,且MySQL服务器提供其自身的哈希函数。必须有一列或多列包含整数值。
上述四种分区类型中,RANGE分区
即范围分区是最常用的。RANGE分区的特点是多个分区的范围要连续,但是不能重叠,默认情况下使用VALUES LESS THAN属性,即每个分区不包括指定的那个值。
以RANGE分区为例,演示分区表
sql
# 创建分区表
mysql> CREATE TABLE `tr` (
-> `id` INT,
-> `name` VARCHAR(50),
-> `purchased` DATE
-> ) ENGINE=InnoDB DEFAULT CHARSET=utf8
-> PARTITION BY RANGE( YEAR(purchased) ) (
-> PARTITION p0 VALUES LESS THAN (1990),
-> PARTITION p1 VALUES LESS THAN (1995),
-> PARTITION p2 VALUES LESS THAN (2000),
-> PARTITION p3 VALUES LESS THAN (2005),
-> PARTITION p4 VALUES LESS THAN (2010),
-> PARTITION p5 VALUES LESS THAN (2015)
-> );
Query OK, 0 rows affected (0.28 sec)
# 插入数据
mysql> INSERT INTO `tr` VALUES
-> (1, 'desk organiser', '2003-10-15'),
-> (2, 'alarm clock', '1997-11-05'),
-> (3, 'chair', '2009-03-10'),
-> (4, 'bookcase', '1989-01-10'),
-> (5, 'exercise bike', '2014-05-09'),
-> (6, 'sofa', '1987-06-05'),
-> (7, 'espresso maker', '2011-11-22'),
-> (8, 'aquarium', '1992-08-04'),
-> (9, 'study desk', '2006-09-16'),
-> (10, 'lava lamp', '1998-12-25');
Query OK, 10 rows affected (0.03 sec)
Records: 10 Duplicates: 0 Warnings: 0
创建后可以看到,每个分区都会对应1个ibd文件。上面创建语句还是很好理解的,在此分区表中,通过YEAR函数取出DATE日期中的年份并转化为整型,年份小于1990的存储在分区p0中,小于1995的存储在分区p1中,以此类推。请注意,每个分区的定义顺序是从最低到最高。为了防止插入的数据因找不到相应分区而报错,我们应该及时创建新的分区。下面继续展示关于分区维护的其他操作。
sql
# 查看某个分区的数据
mysql> SELECT * FROM tr PARTITION (p2);
+------+-------------+------------+
| id | name | purchased |
+------+-------------+------------+
| 2 | alarm clock | 1997-11-05 |
| 10 | lava lamp | 1998-12-25 |
+------+-------------+------------+
2 rows in set (0.00 sec)
# 增加分区
mysql> alter table tr add partition(
-> PARTITION p6 VALUES LESS THAN (2020)
-> );
Query OK, 0 rows affected (0.06 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 拆分分区
mysql> alter table tr reorganize partition p5 into(
-> partition s0 values less than(2012),
-> partition s1 values less than(2015)
-> );
Query OK, 0 rows affected (0.26 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 合并分区
mysql> alter table tr reorganize partition s0,s1 into (
-> partition p5 values less than (2015)
-> );
Query OK, 0 rows affected (0.12 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 清空某分区的数据
mysql> alter table tr truncate partition p0;
Query OK, 0 rows affected (0.11 sec)
# 删除分区
mysql> alter table tr drop partition p1;
Query OK, 0 rows affected (0.06 sec)
Records: 0 Duplicates: 0 Warnings: 0
# 交换分区
# 先创建与分区表同样结构的交换表
mysql> CREATE TABLE `tr_archive` (
-> `id` INT,
-> `name` VARCHAR(50),
-> `purchased` DATE
-> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.28 sec)
# 执行exchange交换分区
mysql> alter table tr exchange PARTITION p2 with table tr_archive;
Query OK, 0 rows affected (0.13 sec)
# 获取分区表信息
SELECT * FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = '' AND TABLE_NAME = '';
普通表转换为分区表
方案1:直接转 (注意,这种方式会锁表,也就是说只能查不能增删改操作)
当单张普通表的数据量超过1亿或者1 TB时,建议使用分区表。这里介绍了使用ALTER语法将单张普通表转换为分区表的注意事项以及示例。
前提条件
普通表中的主键或唯一键需要包含分区表中的分区键字段。
注意事项
普通表转换为分区表的过程中,所有的数据都需要重新进行读写,属于COPY DDL操作。因此,转换时间比较长,且整个转换过程会阻塞当前表上的所有DML操作,所以说,这种方式会锁表,也就是说只能查不能增删改操作。
语法
scss
ALTER TABLE table_name
PARTITION BY RANGE {(expr) | COLUMNS(column_list)}
(partition_definition [, partition_definition] ...);
其中,partition_definition
的定义与各个类型的分区表中的partition_definition
保持一致
示例
以普通表t1
为例,介绍将普通表转换为分区表的操作。
- 创建普通表
t1
。
css
CREATE TABLE t1 (
a int ,
b int ,
Primary Key(a, b));
- 执行以下命令,向表t1中插入数据。
scss
INSERT INTO t1 VALUES(1,1),(2,1),(3,1),(4,1),(111,111),(3333,333);
-
将表
t1
转换为分区表。- 将表
t1
转换为LIST DEFAULT HASH分区表。
sqlALTER TABLE t1 partition BY LIST(a) (PARTITION p0 VALUES IN (1,2,3,4,5), PARTITION p1 VALUES IN (6,7,8,9,10), PARTITION pd DEFAULT PARTITIONS 9);
- 将表
t1
转换为HASH分区表。
sqlALTER TABLE t1 partition BY HASH(a) PARTITIONS 12;
- 将表
t1
转换为RANGE分区表。
sqlALTER TABLE t1 partition by RANGE(a) ( PARTITION p0 VALUES LESS THAN (100), PARTITION p1 VALUES LESS THAN (200) );
- 将表
方案二:创建分区表,全量+增量,应用表切换,案例演示
分区注意事项及适用场景
其实分区表的使用有很多限制和需要注意的事项,参考官方文档,简要总结几点如下:
- 分区字段必须是整数类型或解析为整数的表达式。
- 分区字段建议设置为NOT NULL,若某行数据分区字段为null,在RANGE分区中,该行数据会划分到最小的分区里。
- MySQL分区中如果存在主键或唯一键,则分区列必须包含在其中。
- Innodb分区表不支持外键。
- 更改sql_mode模式可能影响分区表的表现。
- 分区表不影响自增列。
- 最大分区数目不能超过1024。
- 不支持全文索引(FULL TEXT)。
从上面的介绍中可以看出,分区表适用于一些日志记录表。这类表的特点是数据量大、并且有冷热数据区分,可以按照时间维度来进行数据归档。这类表是比较适合使用分区表的,因为分区表可以对单独的分区进行维护,对于数据归档更方便。
分区表为什么不常用
在我们项目开发中,分区表其实是很少用的,下面简单说明下几点原因:
- 分区字段的选择有限制。
- 若查询不走分区键,则可能会扫描所有分区,效率不会提升。
- 若数据分布不均,分区大小差别较大,可能性能提升也有限。
- 普通表改造成分区表比较繁琐。
- 需要持续对分区进行维护,比如到了6月份前就要新增6月份的分区。
- 增加学习成本,存在未知风险。
- 分区表,分区键设计不太灵活,如果不走分区键,很容易出现全表锁。
- 一旦数据量并发量上来,如果在分区表实施关联,就是一个灾难。
- 自己分库分表,自己掌控业务场景与访问模式,可控。分区表,研发写了一个sql,都不确定mysql是怎么玩的,不太可控。
实操篇
以 xroot_order.paid_video_order 为例,实现分区表
统计表数据时间是:2024/05/08,这个功能是 2023年12月上线的。
所以如果是按照年来拆分,感觉数据还是很多。
按照年月拆分的话,一个分区在一两百万的数据
创建 paid_video_order 的分区表
按照每个月一拆分(也可以按照多个月拆分到一个分区)
sql
CREATE TABLE `paid_video_order_partition` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`order_sn` varchar(100) NOT NULL COMMENT '订单编号',
`user_id` bigint(20) NOT NULL COMMENT '用户',
`content_id` varchar(255) NOT NULL COMMENT '剧集壳子ID',
`category` int(11) DEFAULT NULL COMMENT '电影,剧集,短视频',
`episode_id` varchar(255) DEFAULT NULL COMMENT '剧集ID',
`app_name` varchar(500) DEFAULT NULL COMMENT '应用名',
`client_type` varchar(500) DEFAULT NULL COMMENT '渠道包',
`buy_mode` tinyint(2) DEFAULT '0' COMMENT '购买模式: 0 单集 1 全集',
`paid_way` tinyint(2) DEFAULT '0' COMMENT '0 银鱼干购买 1 金鱼干购买 2 广告解锁',
`paid_type` tinyint(2) DEFAULT '0' COMMENT '付费类型: 0 单次扣款 1 自动扣款',
`price` int(11) DEFAULT NULL COMMENT '价格',
`unit_price` varchar(150) DEFAULT NULL COMMENT '单价配置',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '被修改时间',
`deleted` tinyint(1) unsigned DEFAULT '0' COMMENT '逻辑删除 0未删除,1软删除',
`content_name` varchar(255) DEFAULT NULL,
`country` varchar(20) DEFAULT NULL COMMENT '国家',
PRIMARY KEY (`id`,`create_time`),
KEY `idx_createtime` (`create_time`),
KEY `idx_deleted_buyMode_createTime` (`buy_mode`,`create_time`,`deleted`) USING BTREE,
KEY `idx_contentId` (`content_id`(191)) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6055621 DEFAULT CHARSET=utf8mb4 COMMENT='剧购买订单'
PARTITION BY RANGE COLUMNS(create_time)
(PARTITION p0 VALUES LESS THAN ('2024-01-01') ENGINE = InnoDB,
PARTITION p1 VALUES LESS THAN ('2024-02-01') ENGINE = InnoDB,
PARTITION p2 VALUES LESS THAN ('2024-03-01') ENGINE = InnoDB,
PARTITION p3 VALUES LESS THAN ('2024-04-01') ENGINE = InnoDB,
PARTITION p4 VALUES LESS THAN ('2024-05-01') ENGINE = InnoDB,
PARTITION p5 VALUES LESS THAN ('2024-06-01') ENGINE = InnoDB,
PARTITION p202206 VALUES LESS THAN (MAXVALUE) ENGINE = InnoDB);
数据迁移
这里可以通过阿里云的DTS,全量+增量把原表数据同步到新的分区表中。
阿里云dts文档:help.aliyun.com/zh/dts/?spm...
查询数据情况
不跨分区
跨多个分区
跨单个分区
不用分区键查找(查找全部分区,但是能用索引)
应用代码修改表明为分区表
这个没啥好说的,代码里面把表名换了
可能遇到的问题:
以下回复表示普通的读写不会锁所有分区
遇到问题:
对分区建做格式化时
sql
EXPLAIN SELECT * FROM paid_video_order_copy WHERE deleted = 0
AND create_time >= '2024-05-01 00:00:00.0' AND create_time <= '2024-05-07 23:59:59.0';
这个语句时发现查询了所有分区,因为这种RANGE里带表达式的分区裁剪暂时是不支持的(数据库版本是 5.7)。