我这里再补充一点范例用场,须要在异构系统之间须要共享和同步数据的场景,也可以充分利用这项技能。
数据仓库的维度表和事实表的加载一样平常都采取下面的形式:
暴力的全量加载,即删除目标维度表中的所有数据,然后再全量从源表中加载。1. 这种办法会毁坏维度表中的代理键,乃至是不能利用代理键。(例如自增长的ID)。2. 无法保留SCD-Type2即历史类型的变动。

依然利用全量加载,但在实际添补维度表和事实表时候自己来编写利用主键LookUp的比对过程, 找出哪些是目标表中不存在的,哪些是须要更新的。各类办法只能轻松识别新增数据,但是对付更新数据依然须要编写繁芜的比对逻辑,这些逻辑会大幅度降落数据加载的速率。
在源表设计独立的last_update_date字段,用于记录更新韶光。加载过程通过比对这个更新韶光来获取变更/新增的数据。
在笔者的职业生涯中,以上三种办法是最常见的,前两种的巨大缺陷便是全量加载,如果数据集市中的维度表或实时表中存在几十万乃至上百万数据,就要花费数小时加载,乃至方案根本不可行。由于数据的全量加载,还会导致对源表的扫描(在默认隔离级别下)加锁,降落源表的UPDATE/DELETE的性能。第三种方案的缺陷是依赖表架构,有些情形源系统不在掌握范围下,不许可变动表架构,更不可能变动运用程序,同时严重依赖这个字段的准确性,例如这个字段可能被运用程序缺点修正,导致更新加载缺点的数据。
以是,各大数据库厂商早就意识到这种场景,很早就退出了数据变更捕获这项技能(CDC), 可惜重视这项技能的人却不是很多,以是写一篇文章来同大家分享。希望大家在做数据同步,数据仓库,各种数据处理的场景下充分利用这项技能。好了下面就开始一步步先容如何利用SQLServer数据变更捕获。
为数据库启用变更捕获以我自己新建的Test数据库为例,个中包含一个Account的表。
-- ====
-- 为数据启用变更捕获
-- ====
USE Test
GO
EXEC sys.sp_cdc_enable_db
GO为表启用捕获
USE Test
GO
EXEC sys.sp_cdc_enable_table
@source_schema = N'dbo',
@source_name = N'Account', --表名
@role_name = , --这里为了演示不采取角色权限掌握。实际生产环境每每须要分配一个单独的角色,只有这个角色才能利用和获取变更数据。
@supports_net_changes = 1 --启用净捕获,即只保留每行数据的末了变动。
GO查询变更捕获数据
DECLARE @from_lsn binary(10), @to_lsn binary(10)
SET @from_lsn = sys.fn_cdc_get_min_lsn ('dbo_Account');
SET @to_lsn = sys.fn_cdc_get_max_lsn();
SELECT from cdc.fn_cdc_get_net_changes_dbo_Account( @from_lsn, @to_lsn, 'all' );
好了,是不是看的有点懵,我来一个个阐明。上图是运行往后的结果,这个结果来源于为表启用CDC捕获时所创建的影子表,影子表是专门用来存储变更信息的。在系统表下可以看到他们。可以看到影子表便是在源表上增加很多用于识别捕获的列而组成的。
SET @from_lsn = sys.fn_cdc_get_min_lsn ('dbo_Account');
--这一句用于查询被捕获表的最小LSN号,LSN是SQLServer事务日志连续编号,由小到达连续排列。
--sys.fn_cdc_get_min_lsn 用于获取这个LSN,LSN是二进制格式,CDC所有变更捕获只能利用LSN编号进行。
SET @to_lsn = sys.fn_cdc_get_max_lsn();
--这一句是获取日志LSN最大值,这里为什么没有表名?由于最大日志LSN在所有表中都对对齐。例如我在一个事务中对表Account中做了一次更新,同时对另一个捕获表假设Sales表也做了一次更新。那么这两个捕获表的影子表的最大LSN是相同的值,以是没必要在单独通报表名。
SELECT from cdc.fn_cdc_get_net_changes_dbo_Account( @from_lsn, @to_lsn, 'all' );
--这一句是终极调用捕获函数返回结果,参数是两个LSN区间,'all' 代表返回所有的列终极更新,并且在_$update_mask中不标记哪些列被变动了。
好了,再回去看查询出来的结果集的列名含义
再来做一个更新和删除操作,然后看当作果。
update Account
set name = 'Gary3'
where ID = 20
update Account
set name3 = 'Gary name 3'
where id = 19
delete from Account
where id = 18
第一行数据对应 第二条 修正操作。_$operation=4 状态是对的。
第二行数据对应 第三条 删除操作。_$operation=1 状态是对的
第三行数据对应 第一条 修正操作。_$operation=2 ,等等这里为什么是2,由于这条数据是我最早手动添加进去,然后修正的。同时利用的是净变动函数读取 以是他始终认为这条数据是新增。说大略一点,便是我采取的捕获范围是从最小LSN到最大LSN,由于这条数据最小的变动是第一次新增,以是他始终返回的状态是2,但是他的数据是最新的变动。如果想获取新增后的变动,那么须要改变最小LSN。
下表是影子表中所有的记录,可以清晰的看到ID=20的记录 最小LSN是第一行,$operation=2 代表这次事务做的是新增,但是后续ID=20的记录分别做了2次Update,一次是改为Gary,一次是改为Gary3,以是他将终极的Gary3返回了。这便是净变动的意义所在,否则如果返回operation=4的话,这条记录会被认为是变动,那么他新增的状态就丧失落了,这是不对的。我们在做ETL的时候须要知道这条记录时新增的,并且保留他最新的值。
如何知道某一列是否发生了变革?
这也是数据变更捕获最强大的功能之一,如果不该用变更捕获技能,只能用对列利用呆板乏味而丑陋的 oldValue == newValue的技能来判断值是否变动。我们来做一次变动,讲ID=19的记录的名字修正一下。
update Account
set name3 = 'Gary name 4'
where id = 19
我们可以看到影子表(影子表时系统表中_CT结尾的)中末了一条记录反响了本次变动。__$operation =4 ,表示更新。
这里我们须要记录下这次更该的_start_lsn, 由于这次变动的起始lsn变革了,如果还是从最小LSN捕获,那么这条数据只反应初始的状态(这点非常主要)以是我们改用新的start_lsn捕获,这里为了演示方便,我直接硬编码拷贝过来了。
DECLARE @from_lsn binary(10), @to_lsn binary(10),@name int
SET @from_lsn = 0x0000002C000003D60003
SET @to_lsn = sys.fn_cdc_get_max_lsn();
SET @name = sys.fn_cdc_get_column_ordinal
( 'dbo_Account','Name');
SELECT ,
sys.fn_cdc_is_bit_set(@name, __$update_mask) as 'Is_Name_Changed'
from cdc.fn_cdc_get_net_changes_dbo_Account( @from_lsn, @to_lsn, 'all with mask' );
可以看到末了一列is_Name_Changed = 1 的表示Name列被修正了。有了这个标记,我们就可以很随意马虎将 operation=4 $$ is_name_changed = 1作为条件判断列是否发生了更新,在ETL过程中可以利用这个判断 将这个字段作为历史添加到维度表中。
只捕获须要的列例如如下 @captured_column_list 参数可以配置须要捕获的列,而不用默认捕获所有列,这对付性能提高有很大的帮助。
USE Test
GO
EXEC sys.sp_cdc_enable_table
@source_schema = N'dbo',
@source_name = N'Account', --表名
@role_name = , --这里为了演示不采取角色权限掌握。实际生产环境每每须要分配一个单独的角色,只有这个角色才能利用和获取变更数据。
@supports_net_changes = 1 --启用净捕获,即只保留每行数据的末了变动。
@captured_column_list = N'DepartmentID, Name, GroupName',
GO滑动韶光加载
前面讲到的,变更捕获最主要的参数便是_start_lsn, _end_lsn. 大略来说这两个值便是确定读取变更数据的区间,所返回数据是否时是增编削状态,完备由这个区间决定。例如一行数据在更早的区间可能是新增,但是晚一点的区间便是更新或者删除。以是确定好区间非常主要。
好了,那么在真是的ETL加载中,很大程度我们要记录下来每次加载的区间的start_lsn,end_lsn。下次加载我们须要利用上次的end_lsn来打算一个开始lsn.
例如:上次加载完毕后的最大lsn = 0x0000002C000006370004 在这之后 源表又进行了很多次增编削查,
紧接着的一个是lsn = 0x0000002C000006370005, 那么我们下次加载就须要用这个作为出发点,那么如何根据上次的lsn得到下一个紧挨着的lsn呢?
这须要用到 sys.fn_cdc_increment_lsn 函数, 参数是上一次加载完毕后的最大lsn。那么我们改造一下代码。
DECLARE @from_lsn binary(10), @to_lsn binary(10),@name int
SET @from_lsn = sys.fn_cdc_increment_lsn(0x0000002C000003D60003);
SET @to_lsn = sys.fn_cdc_get_max_lsn();
SET @name = sys.fn_cdc_get_column_ordinal
( 'dbo_Account','Name');
SELECT ,
sys.fn_cdc_is_bit_set(@name, __$update_mask) as 'Is_Name_Changed'
from cdc.fn_cdc_get_net_changes_dbo_Account( @from_lsn, @to_lsn, 'all with mask' );
进一步改进,我们须要用一张表来记录每次加载完毕后的lsn,然后查询这个lsn动态通报个代码。假设我已经创建了这个历史表,并且这个历史表始终只有一条记录,便是末了一次加载的lsn. 然我我们把查出来的结果通报给sys.fn_cdc_increment_lsn函数,他会返回这个往后最近的lsn,我们用这个做为出发点lsn进行加载,然后加载完毕 又把最大的@to_lsn存到表Capture_Last_lsn
DECLARE @last_load_lsn binary(10),@from_lsn binary(10), @to_lsn binary(10),@name int
select @last_load_lsn = last_load_lsn from Capture_Last_lsn
SET @from_lsn = sys.fn_cdc_increment_lsn(@last_load_lsn);
SET @to_lsn = sys.fn_cdc_get_max_lsn();
SET @name = sys.fn_cdc_get_column_ordinal
( 'dbo_Account','Name');
SELECT ,
sys.fn_cdc_is_bit_set(@name, __$update_mask) as 'Is_Name_Changed'
from cdc.fn_cdc_get_net_changes_dbo_Account( @from_lsn, @to_lsn, 'all with mask' );
好了,这样我们运行加载的时候总可以早年次加载的出发点往后开始读取变更的数据。
总结数据变更捕获技能是一项非常关键的核心技能,要想设计出可靠的数据捕获传输方案,这可能是唯一的可靠方案,并且非常高效。那么们总结下有点和影响:
优点:
数据库级别供应的可靠性,不用依赖其他第三方技能。
每次只读取捕获的变动,不须要查询源表全部数据。
启用和禁用捕获,对源表透明(唯一须要原表有主键)
捕获是SQLServer内核中利用了复制中的技能捕获事务日志而天生的,所以是异步捕获,对源表影响很小。
对源表的表架构进行变动不毁坏捕获,被删除的列会返回,新增的列也不会涌如今捕获表中。除非重新禁用启用捕获。
问题:
捕获采取SQLServer代理实行job进行捕获和清理。
数据捕获保留的韶光默认为3天,当然可以变动为更永日期。也便是说如果你3天不进行捕获查询,那么SQLServer会自动打消3天前影子表中捕获的修正内容。以是你要谨慎考虑你的ETL数据加载频率,例如1天运行一次捕获查询进行数据同步。由于捕获有效期设置的越长,那么影子表中的数据越多,这对付频繁更新的大表是一个极大寻衅。