之前杨老师的两篇文章《MySQL时间类分区写SQL的一些注意事项》、《MySQL时间分区的实现》,介绍了在MySQL中时间分区的基础知识,这篇文章《MySQL分区表案例分享》给了一个实际的案例,这个是某家互联网公司数据库系统的表调优过程,从实践层面,了解一下。

问题与背景

1. 单张表数据量太大,每天会产生 10W 条记录,一年就是 3650W 条记录。

2. 对这张表的查询95%都是在某一天或者几天内,过滤区间最大不超过一个月。例如在2019年3月1日、2019年4月20日或者是2019年5月1日和2019年5月5日这个时间段内。偶尔会涉及到跨月、跨年查询,但是频率很低。

3. 记录保留10年。也就是单表3.6亿条记录,单表太大,不便于管理,后期如果单表损坏,修复也难。

4. 单表查询性能很差,对历史数据删除性能也很差。

基于以上需求分析后得出结论

1. 查询过滤的数据范围相对比较集中,不是那么分散;要同时考虑过期数据清理性能问题。

2. 考虑把表拆分为10张新表,一张是当前表,剩余9张是历史归档表;当前表存放最近两年的数据,每到年底迁移老旧数据到历史表进行归档,并且对过期历史数据进行清理。

3. 考虑对部分过滤场景使用MySQL分区表,非常适合95%的查询;可以使用分区置换功能把数据移到历史表。

4. 分区表带来几个好处:一是查询性能提升;二是管理方便,过期数据直接快速清理;三是对应用透明,暂时不需要应用改代码。

接下来看看表的优化过程

由于隐私考虑,不方便贴原始表结构,这里用结构简化的示例表来看下优化过程。原始表为pt_old,缩减字段个数到3,记录数缩减10倍为3650W ,每年 365W(客户原来字段有30个,记录数3.6亿),记录范围从2011年到2020年,刚好十年的数据,

(localhost:ytt)<mysql>show create table pt_old\G
*************************** 1. row ***************************Table: pt_old
Create Table: CREATE TABLE `pt_old` (`id` bigint unsigned NOT NULL AUTO_INCREMENT,`r1` int DEFAULT NULL,`log_date` date DEFAULT NULL,PRIMARY KEY (`id`),KEY `idx_log_date` (`log_date`)
) ENGINE=InnoDB AUTO_INCREMENT=64306811 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)(localhost:ytt)<mysql>select min(log_date),max(log_date),count(*) from pt_old;
+---------------+---------------+----------+
| min(log_date) | max(log_date) | count(*) |
+---------------+---------------+----------+
| 2011-01-01    | 2020-12-31    | 36500000 |
+---------------+---------------+----------+
1 row in set (21.14 sec)

先导出原始表数据(按照年导出10份数据),后期直接导入到新分区表,执行以下脚本,

root@ytt-unbuntu:/home/ytt/scripts# cat pt_export
#!/bin/sh
for i in `seq 2011 2020`
do {mysql -D ytt -e "select * from pt_old where log_date between '$i-01-01' and '$i-12-31' into outfile '/var/lib/mysql-files/pt_$i.csv' fields terminated by ',' " } &
done
wait
root@ytt-unbuntu:/home/ytt/scripts# ./pt_export
root@ytt-unbuntu:/var/lib/mysql-files# ls -sihl
总用量 788M
5767677 79M -rw-r----- 1 mysql mysql 79M 2月   4 15:39 pt_2011.csv
5775332 79M -rw-r----- 1 mysql mysql 79M 2月   4 15:42 pt_2012.csv
5775334 79M -rw-r----- 1 mysql mysql 79M 2月   4 15:42 pt_2013.csv
5774596 79M -rw-r----- 1 mysql mysql 79M 2月   4 15:42 pt_2014.csv
5775335 79M -rw-r----- 1 mysql mysql 79M 2月   4 15:42 pt_2015.csv
5775333 79M -rw-r----- 1 mysql mysql 79M 2月   4 15:42 pt_2016.csv
5775329 79M -rw-r----- 1 mysql mysql 79M 2月   4 15:42 pt_2017.csv
5775330 79M -rw-r----- 1 mysql mysql 79M 2月   4 15:42 pt_2018.csv
5775336 79M -rw-r----- 1 mysql mysql 79M 2月   4 15:42 pt_2019.csv
5775331 79M -rw-r----- 1 mysql mysql 79M 2月   4 15:42 pt_2020.csv

分别以年为粒度,建立10张表,其中表pt_2020为分区表,

root@ytt-unbuntu:/home/ytt/scripts# for i in `seq 2011 2020`;do mysql -e"use ytt;create table pt_$i like pt_old;";done;

由于MySQL分区表硬性规定,分区键必须为主键或者主键的一部分,因此将时间字段加到主键里,

(localhost:ytt)<mysql>alter table pt_2020 drop primary key, add primary key (id,log_date);
Query OK, 0 rows affected (0.29 sec)
Records: 0  Duplicates: 0  Warnings: 0

给表pt_2020添加分区(有可能存放当年以及去年的数据,因此要按照天来分区,并且分成两年,这样到了新的一年,就直接把老旧数据迁移出去),修改下之前的存储过程如下,

DELIMITER $$USE `ytt`$$DROP PROCEDURE IF EXISTS `sp_add_partition_pt_current`$$CREATE DEFINER=`root`@`%` PROCEDURE `sp_add_partition_pt_current`(
IN f_year_start YEAR,
IN f_year_end YEAR,
IN f_tbname VARCHAR(64)
)
BEGIN
DECLARE v_days INT UNSIGNED DEFAULT 365;
DECLARE v_year DATE DEFAULT '2011-01-01';
DECLARE v_partition_name VARCHAR(64) DEFAULT '';
DECLARE v_log_date DATE;
DECLARE i,j INT UNSIGNED DEFAULT 1;SET @stmt = '';SET @stmt_begin = CONCAT('ALTER TABLE ',f_tbname,' PARTITION BY RANGE COLUMNS(log_date)(');SET i = f_year_start;WHILE i <= f_year_end DOSET v_year = CONCAT(i,'-01-01');SET v_days = DATEDIFF(DATE_ADD(v_year,INTERVAL 1 YEAR),v_year);SET j = 1;WHILE j <= v_days DOSET v_log_date = DATE_ADD(v_year,INTERVAL j DAY);SET v_partition_name = CONCAT('p',i,'_',LPAD(j,3,'0'));SET @stmt = CONCAT(@stmt,'PARTITION ',v_partition_name,' VALUES LESS THAN(''',v_log_date,'''),');SET j = j + 1;END WHILE;SET i = i + 1;END WHILE;SET @stmt_end = 'PARTITION p_max VALUES LESS THAN (maxvalue))';SET @stmt = CONCAT(@stmt_begin,@stmt,@stmt_end);PREPARE s1 FROM @stmt;EXECUTE s1;DROP PREPARE s1;SELECT NULL,NULL,NULL INTO @stmt,@stmt_begin,@stmt_end;
END$$DELIMITER ;(localhost:ytt)<mysql>call sp_add_partition_pt_current(2020,2021,'pt_2020');
Query OK, 1 row affected (42.11 sec)

分别导入原始数据:2020年的数据导入表pt_2020,其他数据导入到历史表pt_2011到pt_2019,

root@ytt-unbuntu:/home/ytt/scripts# cat pt_import
#!/bin/sh
for i in `seq 2011 2020`
do {mysql -D ytt -e "load data infile '/var/lib/mysql-files/pt_$i.csv' into table pt_$i fields terminated by ',' " } &
done
wait
root@ytt-unbuntu:/home/ytt/scripts# ./pt_import

更改表p_2020为当前表,

(localhost:ytt)<mysql>alter table pt_2020 rename to pt_current;
Query OK, 0 rows affected (0.12 sec)

接下来我们要验证表改造后性能是否符合预期

第一,查询性能分区表要有优势。

第二,分区表的管理、运维效率也要相应提升。

如果这两点都达到要求,就可以直接把分区表改名为原始表,原始表删除。

先来验证查询性能是否有提升
第一条查询:查询'2020-03-01'当天的记录。

基于数据是否被缓存,这里每个查询我执行两次。基于原始表pt_old,第一次查询时间为1分钟1.7秒,第二次为0.03秒;基于分区表pt_current,第一次查询时间为0.02秒,第二次为0.01秒。如果仅对比第一次查询时间,分区表查询性能大幅提升;第二次来讲,相差不多,但分区表查询性能依然领先。

(localhost:ytt)<mysql>select * from pt_old where log_date = '2020-03-01';
...
9593 rows in set (1 min 1.70 sec)
-- 第二次
9593 rows in set (0.03 sec)(localhost:ytt)<mysql>select * from pt_current where log_date = '2020-03-01';
...
9593 rows in set (0.02 sec)
-- 第二次
9593 rows in set (0.01 sec)
第二条查询:查询2020年年底最后5天的记录。

依然每条查询执行两次。基于原始表pt_old的查询时间第一次为2分钟42.21秒,第二次为0.13秒;基于分区表pt_current的查询时间第一次为0.07秒,第二次为0.01秒。两次查询结果,分区表性能的提升都很明显。

(localhost:ytt)<mysql>select * from pt_old where log_date in ('2020-12-27','2020-12-28','2020-12-29','2020-12-30','2020-12-31');
...
30097 rows in set (2 min 42.21 sec)
...
-- 第二次
30097 rows in set (0.13 sec)(localhost:ytt)<mysql>select * from pt_current where log_date in ('2020-12-27','2020-12-28','2020-12-29','2020-12-30','2020-12-31');
...
30097 rows in set (0.07 sec)
...
-- 第二次
30097 rows in set (0.01 sec)

现在来看下管理与运维性能是否有提升

既然用分区表,就会涉及到一个很棘手的问题:每到年底,如何调整分区表来适应新增记录?MySQL并没有直接的方法,不过我们可以利用默认分区p_max来手工扩容。

来看下表p_current的分区数据,

(localhost:ytt)<mysql>select left(partition_name,5) p,sum(table_rows) cnt from information_schema.partitions where table_name = 'pt_current' group by leftt(partition_name,5);
+-------+---------+
| p     | cnt     |
+-------+---------+
| p2020 | 3641722 |
| p2021 |       0 |
| p_max |       0 |
+-------+---------+
3 rows in set (0.02 sec)

目前只有2020年有数据,2021年没有数据,到2021年末记录则会自动加入到分区p_max里。所以应该在2022年1月1日凌晨前得把2020整年的数据挪出去变为pt_2020,并把2022年的分区定义加进去。

那依照我们的分析,我再来写一个自动扩充分区的存储过程,可以配合OS的JOB或者MySQL的EVENT来自动运行,代码如下,

DELIMITER $$USE `ytt`$$DROP PROCEDURE IF EXISTS `sp_autoextend_partition_pt_current`$$CREATE DEFINER=`root`@`%` PROCEDURE `sp_autoextend_partition_pt_current`(
IN f_year YEAR
)
BEGINDECLARE v_days INT UNSIGNED DEFAULT 365;DECLARE v_days_interval DATE DEFAULT '2018-12-31';DECLARE i INT UNSIGNED DEFAULT 1;SET @stmt = '';SET v_days =  DATEDIFF(CONCAT(f_year+1,'-01-01'),CONCAT(f_year,'-01-01'));SET @stmt_begin = 'ALTER TABLE pt_current REORGANIZE PARTITION p_max into(';WHILE i <= v_days DOSET v_days_interval = DATE_ADD(CONCAT(f_year,'-01-01'),INTERVAL i DAY);SET @stmt = CONCAT(@stmt,'PARTITION p',f_year,'_',LPAD(i,3,"0"),' VALUES LESS THAN (''',v_days_interval,'''),');     SET i = i + 1;        END WHILE; SET @stmt_end = 'PARTITION p_max VALUES LESS THAN (maxvalue))';SET @stmt = CONCAT(@stmt_begin,@stmt,@stmt_end);PREPARE s1 FROM @stmt;EXECUTE s1;DROP PREPARE s1;SET @stmt = NULL;SET @stmt_begin = NULL;SET @stmt_end = NULL;   END$$DELIMITER ;

现在来扩充2022年的分区数据,

(localhost:ytt)<mysql>call sp_autoextend_partition_pt_current(2022);
Query OK, 0 rows affected (14.55 sec)

接下来每年底需要做的事情就是把去年的数据挪走,并且删除旧分区定义,添加新的一年分区定义。

现在时间到了2022年,那先给pt_current插入2021年的数据(真实环境里,这部分数据是已经存在的),

(localhost:ytt)<mysql>insert into pt_current (r1,log_date) select r1,date_add(log_date,interval 1 year) from pt_current;
Query OK, 3641722 rows affected (2 min 28.75 sec)
Records: 3641722  Duplicates: 0  Warnings: 0(localhost:ytt)<mysql>select left(partition_name,5) p,sum(table_rows) cnt from information_schema.partitions where table_name = 'pt_current' group by left(partition_name,5);
+-------+---------+
| p     | cnt     |
+-------+---------+
| p2020 | 3641722 |
| p2021 | 3641726 |
| p2022 |       0 |
| p_max |       0 |
+-------+---------+
4 rows in set (0.02 sec)

我们再将2020年的数据挪到历史表,(由于分区表中每年的分区数目较多,为了写法方便,这里我没有用分区置换功能。)

(localhost:ytt)<mysql>create table pt_2020 like pt_old;
Query OK, 0 rows affected (0.05 sec)(localhost:ytt)<mysql>insert into pt_2020 select * from pt_current where log_date between '2020-01-01' and '2020-12-31';
Query OK, 3641722 rows affected (1 min 12.54 sec)
Records: 3641722  Duplicates: 0  Warnings: 0

删除过期数据,

(localhost:ytt)<mysql>SELECT CONCAT('alter table ytt.pt_current drop partition ',partition_name,';') FROM information_schema.`PARTITIONS`  WHERE table_schema = 'ytt' AND table_name = 'pt_current'  AND partition_name like 'p2020%' into outfile '/var/lib/mysql-files/drop_expire_partition_2020.sql';
Query OK, 366 rows affected (0.00 sec)mysql> \. /var/lib/mysql-files/drop_expire_partition_2020.sql
Query OK, 0 rows affected (0.83 sec)
Records: 0  Duplicates: 0  Warnings: 0...Query OK, 0 rows affected (0.82 sec)
Records: 0  Duplicates: 0  Warnings: 0...

需要注意:分区定义一定要有规则,这样有利于后期清理过期数据。

分区表提供了很多的便利,前提是了解他,知道怎么用,做到事半功倍,否则就可能事倍功半了。

近期更新的文章:

《日期字段未定义DATE类型所带来的一些问题》

《为何世界足坛历史射手王是C罗?》

《三种数据库架构的介绍》

《关于数据治理的读书笔记 - 数据治理路线图规划》

《Oracle优化器对谓词顺序处理的一个场景》

文章分类和索引:

《公众号900篇文章分类和索引》

MySQL时间分区案例相关推荐

  1. MySQL时间分区的实现

    无论是在Oracle还是MySQL数据库,又或者是其他关系型数据库,分区表用起来还是很讲究的,像是双刃剑,用好了是宝剑,用不好是锤子. 这是分区相关的历史文章, <truncate分区表的操作, ...

  2. MySQL时间类分区写SQL的一些注意事项

    杨老师上篇文章<MySQL时间分区的实现>介绍了时间类分区的实现方法,这篇是上篇的一个延伸,介绍基于此类分区的相关SQL编写注意事项. 对于分区表的检索无非有两种,一种是带分区键,另一种则 ...

  3. 【mysql分区分表】mysql 按时间分区 【partition】

    大家好,我是烤鸭:     今天分享一下有关 mysql 分区. 需求: 按时间分区. 对千万数据左右的表,进行分区,数据的增加量大概千万/年. 代码实现: 模拟之前已经存在的表: DROP TABL ...

  4. mysql 表分区、按时间函数分区、删除分区、自动添加表分区

    mysql 表分区的几种方式: RANGE分区:基于属于一个给定连续区间的列值,把多行分配给分区. LIST分区:类似于按RANGE分区,区别在于LIST分区是基于列值匹配一个离散值集合中的某个值来进 ...

  5. mysql 时间推移_随着时间的推移可视化COVID-19新案例

    mysql 时间推移 This heat map shows the progression of the COVID-19 pandemic in the United States over ti ...

  6. Mysql删除分区,增加分区,分区数据清理

    Mysql删除分区,增加分区,分区数据清理 最近线上的分区表占用空间较大,需要进行分区数据的删除,记录如下,顺便把分区维护的其他命令也一并记录,方便后续进行查询使用. 之前建表的语句大概如下,省掉了其 ...

  7. 深入理解MySQL——数据库分区

    一.概述 对用户来说,分区表是一个独立的逻辑表,但是底层由多个物理子表组成.实现分区的代码实际上是对一组底层表的句柄对象(Handler Object)的封装.对分区表的请求,都会通过句柄对象转化成对 ...

  8. mysql 不同分区 同时insert_一文看懂mysql数据库分区表概念、类型、适用场景、优缺点及原理...

    概述 最近对项目上部分表按时间做了分区,所以顺便整理下mysql分区表的一些内容,仅供参考. 一.分区表概念 分区是将一个表的数据按照某种方式,比如按照时间上的月份,分成多个较小的,更容易管理的部分, ...

  9. MySQL 表分区详解MyiSam引擎和InnoDb 区别(实测)

    MySQL 表分区详解MyiSam引擎和InnoDb 区别(实测) 一.什么是表分区 通俗地讲表分区是将一大表,根据条件分割成若干个小表.mysql5.1开始支持数据表分区了. 如:某用户表的记录超过 ...

  10. mysql 分区表优化_Sql优化之Mysql表分区

    一  分区表适用于以下场景 1:表非常大以至于无法全部放在内存中,或者只在标的最后部分有热点数据,其他均是历史数据 2:分区表的数据更容易维护.例如想批量删除大量数据可以使用清除整个分区的方式.另外还 ...

最新文章

  1. html 整个页面变灰
  2. uniapp光标自动定义到文本框_如何在Mac上的照片应用中创建自定义日历
  3. linux gt txt内容为空,2gt;/dev/null和gt;/dev/null 2gt;amp;1和2gt;amp;1gt;/dev/null的区别...
  4. SQL server报错42000 - [SQL Server]从数据类型 varchar 转换为 int 时出错。
  5. 大型互联网公司架构演进之路汇总
  6. Easyui入门视频教程 第11集---Window的使用
  7. 信息安全已成社会普遍焦虑 给个人信息加上防护锁
  8. 教你如何防止电脑插入u盘后自动运行
  9. 有头结点单链表的逆置
  10. Bitcoin是什么意思
  11. 架构设计师—你在哪层楼?
  12. Aitit 认证体系之道 attilax著艾龙著 1. 认证体系分类 2 1.1. 按照语言来分 java net php 2 1.2. 按照平台来分 web cs 桌面 2 1.3. 综合性认证
  13. Java 移位操作符
  14. 解压war包并重新编译成war包
  15. Rosalind Java|k-Mer Composition
  16. Excel添加下拉按键自动填充颜色
  17. 思维方式-《金字塔原理》书中的精髓:如何利用金字塔原理,逻辑清晰地思考问题、表达观点。
  18. 优思学院|六西格玛是什么?六西格玛的精髓和原则是什么?
  19. js将中国标准时间转化为年月日时分秒(yyyy-mm-dd)格式以及时间戳,日期,天数之间的转换
  20. 【器件】红外接收二极管和红外接收三极管

热门文章

  1. springboot基于微信小程序的高校学生疫情在校封闭管理系统的设计与实现毕业设计源码240904
  2. 学校 计算机 教室 设计标准,数字美术创新教室建设解决方案(含配套设备)
  3. java毕设项目开源了,springboot+vue的应用级erp系统
  4. 斗战神 琵琶之怨获取攻略
  5. PageHelper.startPage和new PageInfo(list)的一些探索和思考
  6. word论文排版,页码和页眉
  7. python股票量化交易系统源码_经典的股票量化交易策略(含源码)
  8. 前端常见的浏览器兼容性问题及解决方案
  9. 如何用电脑查看自己的IP地址
  10. 设置单行省略的时候在搜狗浏览器里面导致页面布局紊乱