增量计算核心原理:从数据变化说起
增量计算,说白了就是「只算变了的那些」。全量计算像把整锅汤重新煮一遍,增量计算则只往锅里加新料、捞走废渣。我做了这么多年 OLAP,最深的体会就是:增量计算的核心,不在于怎么算,而在于怎么知道「什么变了」。
这一章,我们就来拆解增量计算的四个核心环节。嗯,每个环节我都踩过坑,有些坑现在想起来还肉疼。
增量数据识别机制:怎么找到「新数据」?
增量计算的第一步,是识别出哪些数据是新增的、哪些是修改的、哪些是删除的。我见过不少团队,上来就写复杂逻辑,结果连「哪行变了」都搞不清楚。
常见的识别方式有四种:
| 识别方式 | 原理 | 适用场景 | 我踩过的坑 |
|---|---|---|---|
| 时间戳字段 | 记录每行数据的最后修改时间 | 业务表有统一更新时间 | 时区没对齐,凌晨数据丢了 |
| 自增ID/序列号 | 新数据ID单调递增 | 只追加、不修改的场景 | 分库分表后ID不连续 |
| 版本号字段 | 每次修改版本号+1 | 需要追踪历史版本 | 并发写入时版本冲突 |
| 标志位 | 新增/修改后置标志位 | ETL过程可控 | 忘记重置标志位,重复计算 |
变更数据捕获(CDC)原理:从源头抓变化
CDC,全称 Change Data Capture,翻译过来就是「抓变化」。它不是被动等业务系统通知,而是主动去捕获数据源的每一次变更。
CDC 的实现方式,我按成熟度排个序:
- 基于日志的CDC(最推荐)
解析数据库的 binlog、redo log 或 WAL 日志。比如 MySQL 的 binlog、PostgreSQL 的 WAL、Oracle 的 Redo Log。
优点:无侵入、低延迟、不丢数据。
缺点:需要 DBA 配合开启日志,日志格式解析有学习成本。 - 基于触发器的CDC
在源表上建触发器,每次 INSERT/UPDATE/DELETE 时记录变更到一张日志表。
优点:实现简单,不依赖数据库版本。
缺点:对源库性能有影响,触发器维护成本高。 - 基于查询的CDC
定时轮询源表,通过时间戳或版本号拉取增量数据。
优点:无需额外组件。
缺点:延迟高,对源库有查询压力。
下面这张图,是我常用的 CDC 架构:
增量与全量数据合并策略:新老数据怎么「拼」?
拿到增量数据后,怎么和已有的全量数据合并?这个问题,我见过至少五种方案,但真正好用的就三种。
策略一:覆盖合并(Upsert)
用增量数据直接覆盖全量数据中对应的行。主键相同就更新,主键不存在就插入。
-- 以 ClickHouse 为例
INSERT INTO dwd_order_fact
SELECT
id,
status,
amount,
update_time
FROM stage_orders_incremental
ORDER BY id
-- 注意:ReplacingMergeTree 会在后台去重合并
适用场景:业务表有明确主键,且只关心最新状态。
策略二:追加合并(Append)
增量数据直接追加到全量数据后面,不做覆盖。常用于日志类数据。
-- 追加写入,不做任何去重
INSERT INTO dwd_click_log
SELECT * FROM stage_click_log_incremental;
适用场景:不可变事件流,比如用户行为日志、埋点数据。
策略三:快照合并(Snapshot Merge)
保留全量数据的多个版本,增量数据作为新版本写入。查询时按时间戳取最新版本。
我的经验:快照合并最耗存储,但最安全。我在一个监管报送项目中,要求保留7天内的所有数据变更历史,快照合并是唯一选择。存储成本翻了三倍,但审计一次过。
三种策略的对比:
| 策略 | 存储成本 | 查询性能 | 数据一致性 | 实现复杂度 |
|---|---|---|---|---|
| 覆盖合并 | 低 | 高 | 中(可能丢历史) | 低 |
| 追加合并 | 中 | 高 | 高(只增不改) | 低 |
| 快照合并 | 高 | 中(需过滤版本) | 极高(全历史可回溯) | 高 |
时间戳与版本号管理:给数据打上「时间烙印」
增量计算最怕什么?数据乱序、重复、丢失。时间戳和版本号就是解决这三个问题的利器。
时间戳管理
我习惯在每个增量表中保留三个时间戳字段:
event_time:业务发生时间(业务方写入)process_time:ETL处理时间(系统自动生成)load_time:数据加载时间(入库时间)
为什么要三个?因为业务时间和处理时间经常不一致。有一次,业务系统凌晨补录了昨天的数据,event_time 是昨天,但 process_time 是今天。如果我只用 event_time 做增量判断,那批数据就漏了。
版本号管理
版本号有两种常见模式:
- 全局版本号:整个表一个版本号,每次增量+1。
优点:简单,容易判断数据新鲜度。
缺点:并发写入时版本号可能冲突。 - 行级版本号:每行数据独立版本号。
优点:支持细粒度并发控制。
缺点:版本号管理复杂,存储开销大。
我个人更倾向行级版本号。虽然实现复杂一点,但能精确控制每一行数据的变更顺序。在电商订单场景中,同一笔订单可能同时发生「支付」和「退款」两个操作,行级版本号能保证最终结果正确。
-- 行级版本号示例(伪代码)
CREATE TABLE dwd_order (
order_id String,
status String,
amount Decimal(18,2),
version UInt64, -- 行级版本号
update_time DateTime,
load_time DateTime
) ENGINE = ReplacingMergeTree(version)
ORDER BY (order_id);
-- 增量写入时,版本号由业务系统保证递增
INSERT INTO dwd_order VALUES
('O001', 'paid', 100.00, 3, '2024-01-15 10:00:00', now()),
('O002', 'shipped', 200.00, 1, '2024-01-15 10:01:00', now());