问题背景
在使用 ClickHouse 的 ReplacingMergeTree 引擎时,很多开发者会遇到一个困惑:明明使用了 FINAL
关键字,查询结果却仍然包含重复数据。比如这样的情况:
数据库表
err := db.Table(model.BlockTaskTableName).Set("gorm:table_options", "ENGINE = ReplacingMergeTree(created_at) PARTITION BY intDiv(start_block, 500000) "+"PRIMARY KEY(start_block, created_at, end_block) "+"ORDER BY (start_block, created_at, end_block, status, owner) "+"SETTINGS enable_block_number_column = 1,enable_block_offset_column = 1").AutoMigrate(&model.BlockTask{})if err != nil {panic(fmt.Sprintf("Failed to create block_tasks table in %s: %v", chain, err))}
FInal查询语句
SELECT * FROM test_ethereum.block_tasks FINAL WHERE status = 'init' AND (owner = '' OR owner IS NULL) ORDER BY start_block DESC, created_at DESC LIMIT 100
final查询结果
created_at |updated_at |deleted_at|start_block|end_block|status|owner|assigned_at |completed_at |data_hash|reset_times|blocks_finished|transactions_finished|logs_finished|acc
-----------------------------+-----------------------------+----------+-----------+---------+------+-----+-------------------+-------------------+---------+-----------+---------------+---------------------+-------------+---
2025-09-30 14:55:47.849000000|2025-09-30 14:55:47.849000000| | 23379678| 23379687|init | |1970-01-01 08:00:00|1970-01-01 08:00:00| | 0| 0| 0| 0|
2025-09-30 14:55:46.718000000|2025-09-30 14:55:46.718000000| | 23379678| 23379687|init | |1970-01-01 08:00:00|1970-01-01 08:00:00| | 0| 0| 0| 0|
2025-09-30 14:55:45.544000000|2025-09-30 14:55:45.544000000| | 23379678| 23379687|init | |1970-01-01 08:00:00|1970-01-01 08:00:00| | 0| 0| 0| 0|
2025-09-30 14:55:44.394000000|2025-09-30 14:55:44.394000000| | 23379678| 23379687|init | |1970-01-01 08:00:00|1970-01-01 08:00:00| | 0| 0| 0| 0|
2025-09-30 14:55:43.182000000|2025-09-30 14:55:43.182000000| | 23379678| 23379687|init | |1970-01-01 08:00:00|1970-01-01 08:00:00| | 0| 0| 0| 0|
2025-09-30 14:55:42.029000000|2025-09-30 14:55:42.029000000| | 23379678| 23379687|init | |1970-01-01 08:00:00|1970-01-01 08:00:00| | 0| 0| 0| 0|
2025-09-30 14:55:40.887000000|2025-09-30 14:55:40.887000000| | 23379678| 23379687|init | |1970-01-01 08:00:00|1970-01-01 08:00:00| | 0| 0| 0| 0|
2025-09-30 14:55:39.710000000|2025-09-30 14:55:39.710000000| | 23379678| 23379687|init | |1970-01-01 08:00:00|1970-01-01 08:00:00| | 0| 0| 0| 0|
2025-09-30 14:55:38.516000000|2025-09-30 14:55:38.516000000| | 23379678| 23379687|init | |1970-01-01 08:00:00|1970-01-01 08:00:00| | 0| 0| 0| 0|
返回的结果中,相同的 start_block
和 end_block
组合出现了多次,完全没有去重效果。
问题根源:ORDER BY 的错误理解
ReplacingMergeTree 的工作原理
ReplacingMergeTree
的去重机制基于两个关键部分:
-
版本字段:在引擎声明中指定,如
ReplacingMergeTree(created_at)
-
去重依据:由
ORDER BY
子句定义的字段组合
核心规则:当 ORDER BY
的所有字段都相同时,ClickHouse 才认为这些记录是"重复数据",然后根据版本字段保留最新版本。
错误的表结构设计
-- 错误的设计!
ENGINE = ReplacingMergeTree(created_at)
ORDER BY (start_block, created_at, end_block, status, owner)
这里的问题在于:版本字段 created_at
出现在了 ORDER BY
子句中。
为什么这样设计是错误的?
假设有以下数据:
start_block | created_at | end_block | status | owner |
---|---|---|---|---|
23379678 | 2025-09-30 14:55:47 | 23379687 | init | |
23379678 | 2025-09-30 14:55:46 | 23379687 | init | |
23379678 | 2025-09-30 14:55:45 | 23379687 | init |
在 ClickHouse 看来,这些是完全不同的记录,因为:
-
(23379678, 2025-09-30 14:55:47, 23379687, init, )
-
(23379678, 2025-09-30 14:55:46, 23379687, init, )
-
(23379678, 2025-09-30 14:55:45, 23379687, init, )
由于 created_at
不同,每条记录的 ORDER BY
键都不同,因此永远不会触发去重机制。
正确的解决方案
1. 修正表结构
-- 正确的设计
ENGINE = ReplacingMergeTree(created_at)
ORDER BY (start_block, end_block, status, owner)
现在,相同的 (start_block, end_block, status, owner)
组合被认为是重复数据,系统会保留其中 created_at
最大的版本。
2. Go 代码示例
err := db.Table(model.BlockTaskTableName).Set("gorm:table_options", "ENGINE = ReplacingMergeTree(created_at) "+"PARTITION BY intDiv(start_block, 500000) "+"PRIMARY KEY(start_block, end_block) "+"ORDER BY (start_block, end_block, status, owner) "+ // 关键:移除 created_at"SETTINGS enable_block_number_column = 1, enable_block_offset_column = 1").AutoMigrate(&model.BlockTask{})
3. 验证修正效果
修改后,相同的测试数据:
start_block | created_at | end_block | status | owner |
---|---|---|---|---|
23379678 | 2025-09-30 14:55:47 | 23379687 | init | |
23379678 | 2025-09-30 14:55:46 | 23379687 | init | |
23379678 | 2025-09-30 14:55:45 | 23379687 | init |
现在 ClickHouse 认为这些是相同的记录(因为 ORDER BY
键相同),最终只保留 created_at = 2025-09-30 14:55:47
的那条记录。
重要注意事项
1. 后台合并的异步性
即使表结构正确,ReplacingMergeTree
的去重也是后台异步进行的:
-- 手动触发合并(生产环境慎用)
OPTIMIZE TABLE block_tasks FINAL;
2. 实时精确查询的替代方案
如果业务要求100%实时精确,建议使用聚合查询替代 FINAL
:
-- 方法1:使用子查询
SELECT *
FROM block_tasks
WHERE (start_block, end_block, created_at) IN (SELECT start_block, end_block, MAX(created_at)FROM block_tasks WHERE status = 'init'GROUP BY start_block, end_block
);-- 方法2:使用窗口函数
SELECT * FROM (SELECT *,ROW_NUMBER() OVER (PARTITION BY start_block, end_block ORDER BY created_at DESC) as rnFROM block_tasks WHERE status = 'init'
) WHERE rn = 1;
最佳实践总结
-
版本字段不入 ORDER BY:
ReplacingMergeTree(version_field)
中的版本字段不应出现在ORDER BY
子句中 -
ORDER BY 定义去重粒度:
ORDER BY
字段的组合决定了什么算是"重复数据" -
高基数字段在前:在
ORDER BY
中将高基数字段(如ID、时间戳)放在前面 -
接受最终一致性:
ReplacingMergeTree
适合能接受短暂数据延迟的场景 -
实时需求用聚合:需要实时精确去重时,使用聚合查询而非
FINAL
结论
ReplacingMergeTree
的去重失效通常不是因为 FINAL
关键字的问题,而是由于表结构设计不当。记住这个简单的原则:版本字段决定保留谁,ORDER BY 字段决定谁是重复。正确区分这两者的角色,就能避免这个常见的陷阱。
通过本文的解决方案,你的 FINAL
查询将能够正确工作,返回真正去重后的数据结果。