cdc.fn_cdc_get_allchanges* 不适用于大事务,因其按__$start_lsn+__$seqval返回、不保证事务内变更原子聚合,且存在隐式行数限制与超时截断风险,易导致镜像错位或语义丢失。

为什么不能直接用 cdc.fn_cdc_get_all_changes_* 查大事务
大事务(比如单次更新百万行)在事务日志中会生成大量连续 LSN,但 cdc.fn_cdc_get_all_changes_* 函数默认按 __$start_lsn + __$seqval 顺序返回结果,不保证单事务内所有变更原子聚合。更关键的是:该函数有隐式行数限制(尤其在 SQL Server 2016+ 中受内存和查询超时影响),大事务可能被截断、分片到多个调用中,导致下游解析出错或丢失操作语义(例如 UPDATE 的前镜像/后镜像错位)。
必须用 __$start_lsn + __$tran_end_time 聚合事务边界
SQL Server CDC 的 _CT 表本身不存事务结束时间,但可通过关联 sys.fn_cdc_map_lsn_to_time 和 sys.fn_cdc_get_max_lsn 推导出近似事务窗口;更可靠的做法是先查 cdc.lsn_time_mapping(需确保 sys.sp_cdc_add_job 已启用 cleanup job 并运行过),再 JOIN 到 _CT 表:
SELECT
t.__$start_lsn,
t.__$operation,
t.__$seqval,
l.tran_end_time,
COUNT(*) OVER (PARTITION BY t.__$start_lsn, l.tran_end_time) AS tran_row_count
FROM cdc.dbo_sys_order_info_CT t
INNER JOIN cdc.lsn_time_mapping l
ON t.__$start_lsn = l.start_lsn
WHERE t.__$start_lsn >= <code>sys.fn_cdc_get_min_lsn('dbo_sys_order_info')</code>
AND l.tran_end_time IS NOT NULL
注意:lsn_time_mapping 只在 cleanup job 运行后才填充,若为空,说明 job 未执行或失败 —— 此时不能依赖时间字段做聚合。
@supports_net_changes = 1 不适合大事务增量消费
启用 net changes(即建表时设 @supports_net_changes = 1)会生成 cdc.fn_cdc_get_net_changes_* 函数,它把同一事务内的 INSERT+UPDATE+DELETE 合并为最终状态,但代价是:它强制扫描整个事务范围内的所有变更行,对大事务极易触发 tempdb spill 或超时。实际生产中,大事务场景应禁用 net changes,改用 all changes 模式,并自行按 __$start_lsn 分批处理:
- 每次只取一个
__$start_lsn对应的所有行(用WHERE __$start_lsn = @lsn+ORDER BY __$seqval) - 用
TOP (5000)分页避免单次结果集过大 - 记录已处理的最高
__$start_lsn和对应最大__$seqval,作为下一轮起点
大事务下的 LSN 推进必须跳过中间值
很多人误以为调用 sys.fn_cdc_increment_lsn(@last_lsn) 就能安全推进到下一个事务 —— 错。大事务的 LSN 是连续块,increment_lsn 只加 1,大概率落在同一事务内部,导致重复消费。正确做法是:查出当前已处理事务的最大 __$start_lsn,再用 sys.fn_cdc_get_max_lsn() 获取全局最新 LSN,然后用 sys.fn_cdc_get_min_lsn('capture_instance') 确认下一轮起始点是否仍在有效窗口内。中间缺失的 LSN 不代表数据丢失,而是尚未提交或已被 cleanup job 清理 —— 这种情况必须靠重试机制兜底,不能靠 LSN 线性递增假设。










