一、背景
对于订单这类具有时间属性的数据,会随时间累积,数据量越来越多,为了提升查询性能需要对数据进行拆分,首选的拆分方法是把旧数据归档到历史表中去。这种拆分方法能起到很好的效果,更重要的是对系统的改动小,升级成本低。
很多大厂早年就是靠着此策略,撑了好几年。还记得淘宝订单搜索页面”只能查询仅此三个月订单“么?
二、归档历史数据
所谓归档,其实也是一种拆分数据的策略。简单地说,就是把大量的历史订单移到另外一张历史订单表中。为什么这么做呢?因为像订单这类具有时间属性的数据,都存在热尾效应。大多数情况下访问的都是最近的数据,但订单表里面大量的数据都是不怎么常用的老数据。
因为新数据只占数据总量中很少的一部分,所以把新老数据分开之后,新数据的数据量就会少很多,查询速度也就会快很多。老数据虽然和之前比起来没少多少,查询速度提升不明显,但是,因为老数据访很少会被访问到,所以慢一点儿也问题不大。
1.好处
拆分订单时,2需要改动的代码非常少
不需要修改的代码
- 大部分对订单表的操作都是在订单完成之前,这些业务逻辑都是完全不用修改的。
- 像退货退款这类订单完成后的操作,也是有时限的,那这些业务逻辑也不需要修改
需要修改的代码
- 查询统计类功能,会查历史订单
按照时间,选择去订单表还是历史订单表查询就可以了。
2.具体操作方式
2.1不可以停服务
- 创建于订单表结构一致的历史订单表
- 把订单表中数据分批查询出来,插入历史订单表中。
技术手段不限,不过如果是数据库是主从分离的。请在从库中查询订单数据,再写到主库历史订单表中去,可以减少一些主库的压力 - 不要着急删除老订单数据,先验证新代码是否有bug,是否支持线上业务。
- 不要着急删除老订单数据,先验证新代码是否有bug。
- 不要着急删除老订单数据,先验证新代码是否有bug, 是否支持线上业务。
如果有bug 可以理解回滚代码,不至于影响线上业务。 - 等新版本代码上线并验证无误之后,就可以删除订单表中的历史订单数据了。
- 上线一个定时脚本或定时任务,定期把过期订单数据从订单表搬到历史订单表中。
与订单表相关的订单子表也作如上操作,毕竟是根据订单表id关联,伴随增长的。
2.2可以停服务
简单粗暴一点,近乎于重建了一次订单表,不需要漫长的删除历史订单的过程了。
- 直接新建一张临时订单表
- 将当前订单表数据复制到临时订单表中
- 把旧的订单表改名,最后把临时订单表的表名改成正式订单表
-- 新建一个临时订单表
create table orders_temp like orders;
-- 把当前订单复制到临时订单表中
insert into orders_temp
select * from orders
where timestamp >= SUBDATE(CURDATE(),INTERVAL 3 month);
-- 修改替换表名
rename table orders to orders_to_be_droppd, orders_temp to orders;
-- 删除旧表
drop table orders_to_be_dropp
三、 删除大量数据
一次性删除大量数据,会提示删除错误。所以需要分批删除,以下以每1000条记录删除,建议执行一次停顿一会,不要给数据库造成太大压力。
delete from orders
where timestamp < SUBDATE(CURDATE(),INTERVAL 3 month)
order by id limit 1000;
sql优化 : 每次都要进行对大于时间戳的所有数据进行排序,其实没有必要
# 查询符合的最大id
select max(id) from orders
where timestamp < SUBDATE(CURDATE(),INTERVAL 3 month);
# 符合小于id的数据进行排序,这数据量就减少很多
delete from orders
where id <= ?
order by id limit 1000;
为什么要排序呢?
为什么在删除语句中非得加一个排序呢?因为按 ID 排序后,我们每批删除的记录,基本都是 ID 连续的一批记录,由于 B+ 树的有序性,这些 ID 相近的记录,在磁盘的物理文件上,大致也是放在一起的,这样删除效率会比较高,也便于 MySQL 回收页。
四、 磁盘整理
大量的历史订单数据删除完成之后,如果你检查一下 MySQL 占用的磁盘空间,你会发现它占用的磁盘空间并没有变小,这是什么原因呢?
只是指针标记为空闲,实际存储单元未释放。 一切为了速度
虽然逻辑上每个表是一颗 B+ 树,但是物理上,每条记录都是存放在磁盘文件中的,这些记录通过一些位置指针来组织成一颗 B+ 树。当 MySQL 删除一条记录的时候,只能是找到记录所在的文件中位置,然后把文件的这块区域标记为空闲,然后再修改 B+ 树中相关的一些指针,完成删除。其实那条被删除的记录还是躺在那个文件的那个位置,所以并不会释放磁盘空间。
这么做也是没有办法的办法,因为文件就是一段连续的二进制字节,类似于数组,它不支持从文件中间删除一部分数据。如果非要这么删除,只能是把这个位置之后的所有数据往前挪,这样等于是要移动大量数据,非常非常慢。所以,删除的时候,只能是标记一下,并不真正删除,后续写入新数据的时候再重用这块儿空间。
对于InnoDB 释放磁盘存储
OPTIMIZE TABLE tablename
OPTIMIZE TABLE 注意点:
- 把这个表重建一遍,执行过程中会一直锁表,也就是说这个时候下单都会被卡住
- MySQL 的配置必须是每个表独立一个表空间(innodb_file_per_table = ON),如果所有表都是放在一起的,执行 OPTIMIZE TABLE 也不会释放磁盘空间。
- 索引也会重建
数据和索引都会变得紧凑,不仅占用磁盘空间减少,查询效率还提升不少。