这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 ReplacingMergeTree 用来处理“同一业务键可能被多次写入”的数据。它在后台合并时按排序键去重,并可结合版本列保留最新记录。它不是实时唯一索引,查询侧仍要正确处理版本。
8.1 适用场景
适合:
- MySQL 表同步;
- CDC 变更流;
- 订单、用户、商品等状态快照;
- 任务失败重试造成的数据重放;
- 上游补数和修正。
不适合:
- 需要强一致事务;
- 高频单键实时更新;
- 每次查询都必须物理去重;
- 业务键本身无法稳定表达唯一性。
8.2 建表
CREATE TABLE analytics.orders_local
(
order_date Date,
order_id String,
user_id UInt64,
order_status LowCardinality(String),
amount Decimal64(2),
updated_at DateTime,
version UInt64
)
ENGINE = ReplacingMergeTree(updated_at)
PARTITION BY toYYYYMM(order_date)
ORDER BY (order_date, order_id)
SETTINGS index_granularity = 8192;
版本列可以是 DateTime 或 UInt64。建议版本来源稳定、单调递增,并能覆盖同秒内的多次修改。
8.3 去重键
去重范围由 ORDER BY 决定,不是某个显式 PRIMARY KEY 猜测值。
ORDER BY (order_date, order_id)
表示同一个分区中,order_date + order_id 相同的行会在合并时被替换。
如果 order_date 会随状态更新而改变,同一订单可能出现在两个日期下,去重就不符合预期。因此同步链路必须固定业务日期字段或把 order_date 排除在更新字段外。
8.4 版本语义
写入示例:
INSERT INTO analytics.orders_local VALUES
('2026-08-25', 'O001', 1001, 'CREATED', 100.00, '2026-08-25 10:00:00', 1),
('2026-08-25', 'O001', 1001, 'PAID', 100.00, '2026-08-25 10:05:00', 2),
('2026-08-25', 'O001', 1001, 'CANCELLED', 100.00, '2026-08-25 10:03:00', 3);
如果版本列是 updated_at,第三行版本不是最大值,最终保留第二行。因此不能只看插入顺序,必须确认版本定义。
推荐:
| 版本来源 | 说明 |
|---|---|
| upstream version | 业务库版本号,优先 |
| binlog 时间戳 | 注意乱序和精度 |
| 事件生成时间 | 可能与入库时间不同 |
| ETL 批次号 | 同批内多次更新要处理 |
8.5 查询最新状态
合并前可能仍有重复,稳妥查询:
SELECT
order_date,
order_id,
argMax(user_id, version) AS user_id,
argMax(order_status, version) AS order_status,
argMax(amount, version) AS amount,
max(updated_at) AS updated_at
FROM analytics.orders_local
WHERE order_date = '2026-08-25'
GROUP BY order_date, order_id;
argMax(value, version) 返回版本最大行的 value。不要使用 max(order_status),状态字符串大小与业务状态无关。
8.6 FINAL
SELECT *
FROM analytics.orders_local
FINAL
WHERE order_date = '2026-08-25';
FINAL 会让查询在读取阶段应用合并语义,可能增加 CPU、内存和执行时间。使用原则:
- 先过滤分区和键范围;
- 小范围查询可以使用;
- 大报表优先物化最新状态;
- 定位慢查询时检查是否 FINAL 导致。
8.7 手动合并
OPTIMIZE TABLE analytics.orders_local PARTITION 202608;
更彻底:
OPTIMIZE TABLE analytics.orders_local PARTITION 202608 FINAL;
手动 FINAL 合并会强制处理 part,成本高。适合维护窗口或小分区修复,不适合业务高峰循环执行。
8.8 与副本和分布式表组合
本地表:
CREATE TABLE analytics.orders_shard
(
order_date Date,
order_id String,
order_status LowCardinality(String),
updated_at DateTime,
version UInt64
)
ENGINE = ReplicatedReplacingMergeTree(
'/clickhouse/tables/{shard}/orders',
'{replica}',
version
)
PARTITION BY toYYYYMM(order_date)
ORDER BY (order_date, order_id);
分布式表:
CREATE TABLE analytics.orders_all
AS analytics.orders_shard
ENGINE = Distributed(
analytics_cluster,
analytics,
orders_shard,
cityHash64(order_id)
);
查询分布式表时 FINAL 的下发行为与版本和查询方式有关。核心是保证同一个 order_id 落到同一分片,否则不同分片各有一份记录,无法全局替换。
8.9 同步链路设计
MySQL binlog / CDC
|
v
Kafka
|
v
ETL 解析、补版本、映射日期
|
v
ReplacingMergeTree
|
v
最新状态视图 / 汇总表
必须明确:
- 主键字段;
- 不可变业务日期;
- 版本字段;
- 删除事件表达;
- 延迟消息处理;
- 重放窗口;
- 对账规则。
8.10 对账与排查
检查重复:
SELECT order_id, count() AS c
FROM analytics.orders_local
WHERE order_date = '2026-08-25'
GROUP BY order_id
HAVING c > 1
ORDER BY c DESC;
检查最新状态:
SELECT order_id, max(version), argMax(order_status, version)
FROM analytics.orders_local
GROUP BY order_id
ORDER BY order_id;
常见问题:
| 现象 | 原因 |
|---|---|
| 重复一直存在 | 尚未合并,属正常 |
| 旧状态胜出 | 版本列错误或乱序 |
| 同键跨分区 | 排序键含可变日期 |
| 分布式仍重复 | 分片路由改变或同键跨分片 |
| 查询变慢 | 大范围 FINAL |
本章小结
ReplacingMergeTree 用后台合并降低物理重复,用版本列表达最新状态。正确的排序键和稳定版本是设计核心;查询侧使用 argMax 或受限 FINAL 是一致性保障。它解决的是幂等分析存储,不是实时事务更新。
思考题
- ReplacingMergeTree 的去重键由什么决定?
- 版本列为什么必须单调可信?
- FINAL 有什么代价?
- 分片路由变化如何影响去重?
- 如何为 CDC 链路设计版本和对账?