关于历史数据迁移这事,相信很多数据库管理员都经常要面对,因为应对的场景和数据量的不同,数据迁移方法和技术也
五花八门;在一个数据量不大的表上迁移数据一般都不会有什么问题,一旦表的数据量增大,而且应用对这个表访问非常频繁时,
那如何有效的迁移数据就会是一件很有讲究的事。
迁移时先考虑以下几点要求:
第一:设计历史数据保持方法:
这个是个比较泛的概念,不同的公司使用的方式都不一样,常见有以下几种:
其实就是在库中新建一个同结构的历史表,如:Product_History,然后将历史数据都放入到这个表中,一般适合数据量不
大的情况;
优劣:优点是比较简单,便于维护,查询的时候通过View将两个表Union起来;缺点是如果数据量比较大时,会有很大的
性能问题(包括查询和插入);
可能是按一个月或者一年新建一个历史表,如:Product_2010、Product_2011、Product_2012…等,然后将不同时期的
历史数据存放到不同的历史表中;这个方式比较常见,查询的时候通过View将这些表Union All起来。
优劣:优点是按日期分表后,会将数据量横向切小,单个表的数据量一般不会太大,处理起来性能比较好;缺点是维护比较
麻烦,尤其是有字段的增删改的时候,所有的历史表都需要跟着修改。
这种方式很多有BI部门的公司采用,会将历史数据和当前数据都定期存放到数据仓库中,不仅可以给BI提供制作报表的数据,
前台用户查询历史数据,APP程序就可以直接定位到仓库去查询,不影响产品环境。
优劣:优点很明显,不仅解决了历史数据查询对产品环境的影响和BI报表数据来源的问题,而且减轻了生产库保存这些历史数
据的负担;缺点是需要有BI部门,也就意味着需要相关的软、硬和人员的投入。
第二: 数据插入和删除:
数据插入这个还是比较好解决的,我们可以直接用insert插入,也可以用BCP,Bulk Insert、SSIS等方式;数据删除在数据量
比较大的时候比较棘手,主要是要权衡系统影响和速度的问题,如果每次删除的数据量比较大,势必会对系统使用者造成影响,如果
每次删除的数据量比较小,可能需要比较长的时间才能完成(分区表有直接删掉某个分区的操作,这样可以不用删除数据,直接删掉
分区就行,在此不做说明,可以参考相关文档);
第三: 系统影响的考虑:
这个和第二点相关,主要是处在系统删除的时候,需要考虑到锁的情况,避免数据库出现大批量的阻塞。
第四: Job运行时间和预计的搬移时间控制:
JOB运行时间毫无疑问是要放到系统业务的低峰时期(一般都是晚上),不过前期我们应该知道我们的系统在什么时间段内业务
比较少,这段时间里面就适合做数据的搬移操作;控制搬移时间,这个要求我们对数据搬移进行控制,如果达到某个时间点(预计业
务开发繁忙了),数据还没有搬移完成,那么应该中断搬移过程,以免影响到业务的正常进行;如果出现这种数据不能按时搬移完成
的情况,意味着需要调整搬移的过程,或者使用更高效的方法,防止历史数据越积越多。
第五:主键冲突问题:
基于对系统的影响考虑,我们一般在查询数据时都采用了With(nolock) 的方式,而数据库在默认的Read Commited隔离级
别下,加上With(nolock) 效果和Read UnCommited一样,这样在搬移数据时,可能会造成将还没有提交的数据搬移到历史表的情
况,在下次运行时,便产生了主键冲突的问题;有以下几种方法能避免此种情况:
我们以下面的两个表为例,对一些常用的数据搬移方法(将相关的方法建到JOB或者SP里面调用就行了),进行说明:
--Master table
create table Product
(
Id int identity(1,1) primary key,
Name varchar(100),
Product_Desc varchar(1000),
Price decimal(12,2),
CreateTime datetime,
UpdateTime datetime,
Statu tinyint
)
--index
create index ix_createtime on Product(CreateTime) with(fillfactor=90,online=on)
--History
create table Product_History
(
Id int primary key, --没有自增属性
Name varchar(100),
Product_Desc varchar(1000),
Price decimal(12,2),
CreateTime datetime,
UpdateTime datetime,
Status tinyint
)
--index
create index ix_createtime on Product_History (CreateTime) with(fillfactor=90,online=on)
No1. 简单的数据搬移:
简单的历史数据迁移方法,我们直接新建一个同结构的表,命名为表名加History,然后按照条件,每天搬移一部分数据到历史表中即可;
例如:我们将下面Product表的数据搬移到Product_History历史表中:
--搬移条件
--10天前,Status=0的Product
insert into Product_History
select * from Product with(nolock)
where CreateTime<convert(varchar,DATEADD(day,-10,GETDATE()),112) and Status=0
--删除条件
delete from Product
where CreateTime<convert(varchar,DATEADD(day,-10,GETDATE()),112) and Status=0
No2. 方法一的改进:
对于数据量不大的表,我们为节省时间,可以直接把搬移和删除两个步骤放到一起来进行,方法如下:
--合并搬移和删除条件
delete a
output deleted.* into Product_History
from Product a
where CreateTime<convert(varchar,DATEADD(day,-10,GETDATE()),112) and Status=0
No3. 分步搬移法(减轻对系统影响):
方法一和二在数据量比较小时是没有问题的,搬移过程能很快完成,但是一旦数据量变大,数据搬移可能需要一段时间才能完成的情况
下,如果再采用这种方式,势必对访问这个表的APP造成大量阻塞,这个是在线系统不能接受的,于是我们不得不把系统影响考虑进来,
把大的数据量分批次来搬移,方法如下:
--------------------------分步搬移---------------------------------
while(1=1)
begin
declare @productid table
(
id int primary key
)
insert into @productid
select top 1000 id from Product with(nolock)
where CreateTime<convert(varchar,DATEADD(day,-10,GETDATE()),112) and Status=0
if exists(select 1 from @productid )
begin
--搬移条件
insert into Product_History
select * from Product with(nolock)
where id in(select id from @productid)
--删除条件
delete from Product
where id in(select id from @productid)
waitfor delay '00:00:02'
end
else
break;
end
--------------------------------------------------------------------
---------------------------分步搬移,合并搬移和删除条件------------------
--分步搬移,合并搬移和删除条件
while(1=1)
begin
delete top (1000) a
output deleted.* into Product_History
from Product a
where CreateTime<convert(varchar,DATEADD(day,-10,GETDATE()),112) and Status=0
if @@ROWCOUNT=0
break
else
waitfor delay '00:00:02' --delay 2s
end
---------------------------------------------------------------------
No4. 控制搬移时间:
我们在第三步的基础上做修改,为了防止搬移作业在业务高峰时间还在运行,阻塞业务操作,我们需要在搬移进行到某个时间点时,
中断搬移作业的运行,方法如下:
---------------------------7点后终止运行------------------
while(1=1)
begin
delete top (1000) a
output deleted.* into Product_History
from Product a
where CreateTime<convert(varchar,DATEADD(day,-10,GETDATE()),112) and Status=0
if @@ROWCOUNT=0 or datepart(HOUR, GETDATE())>=7 --7点后终止运行
break
else
waitfor delay '00:00:02' --delay 2s
end
---------------------------------------------------------------------
No5. 了解搬移的进度:
有时候我们看到搬移JOB一直在运行,但是没有办法知道它要运行到什么时候才能运行完,只能干等,于是我们有对某些搬移作业进
度情况作了解的需求,我们再来更改脚本:
--先创建一个搬移记录表
create table temp_history_2011_12_01
( prd_id int primary key,
is_del bit --是否搬移完成
)
--------------------------了解搬移进度---------------------------------
truncate table temp_history_2011_12_01
--所有需要搬移的数据插入到表中
insert into temp_history_2011_12_01
select id,0 from Product with(nolock)
where CreateTime<convert(varchar,DATEADD(day,-10,GETDATE()),112) and Status=0
while(1=1)
begin
declare @productid table
(
id int primary key
)
--1000
insert into @productid
select top 1000 prd_id from temp_history_2011_12_01 where is_del=0
if exists(select 1 from @productid )
begin
--搬移条件
insert into Product_History
select * from Product with(nolock)
where id in(select id from @productid)
--删除条件
delete from Product
where id in(select id from @productid)
--update is_del
update temp_history_2011_12_01 set is_del=1
where prd_id in(select id from @productid)
waitfor delay '00:00:02'
end
else
break;
end
truncate table temp_history_2011_12_01
---------------------------------------------------------------------
--------------假定已运行时间是5小时,估计剩余时间-------------
declare @fin_count int,@need_count int
select @fin_count=COUNT(0) from temp_history_2011_12_01 with(nolock) where is_del=1
select @need_count=COUNT(0) from temp_history_2011_12_01 with(nolock) where is_del=0
select @need_count*1.0/@fin_count*5
---------------------------------------------------------------------
No6. 避免重复键冲突:
我们先来看下ignore_dup_key作用的描述:使涉及多行的insert语句在有重复键值的错误时不会引起整个语句的回滚,那个导致不唯一的行会被舍弃, 而其他的行都将被插入或者更新,同样可以在创建主键约束时,使用该选项。
于是,我们可以在历史表创建时,在主键上使用这个选项,例如:
CREATE TABLE [dbo].[Product_History](
[Id] [int] NOT NULL,
[Name] [varchar](100) NULL,
[Product_Desc] [varchar](1000) NULL,
[Price] [decimal](12, 2) NULL,
[CreateTime] [datetime] NULL,
[UpdateTime] [datetime] NULL,
[Status] [tinyint] NULL,
PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (IGNORE_DUP_KEY = ON) ON [PRIMARY]
) ON [PRIMARY]
这样就可以避免因主键冲突导致数据搬移JOB的失败了。
以上是就SQL层面的历史数据搬移的一些方法,还有BCP、Bulk Insert、SSIS等方法可以使用,在此不做说明;以上方法加以
调整,便可以做成比较通用的数据搬移方法,包括控制搬移记录数、控制搬移时间、了解搬移进度等问题都可以得到灵活控制,让数
据搬移顺起来。