ClickHouse ReplacingMergeTree 去重陷阱:为什么你的 FINAL 查询无效? - 若

news/2025/9/30 15:46:34/文章来源:https://www.cnblogs.com/zhanchenjin/p/19121289

问题背景

在使用 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 的去重机制基于两个关键部分:

  1. 版本字段:在引擎声明中指定,如 ReplacingMergeTree(created_at)

  2. 去重依据:由 ORDER BY 子句定义的字段组合

核心规则:当 ORDER BY 的所有字段都相同时,ClickHouse 才认为这些记录是"重复数据",然后根据版本字段保留最新版本。

错误的表结构设计

sql
-- 错误的设计!
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. 修正表结构

sql
-- 正确的设计
ENGINE = ReplacingMergeTree(created_at)
ORDER BY (start_block, end_block, status, owner)

现在,相同的 (start_block, end_block, status, owner) 组合被认为是重复数据,系统会保留其中 created_at 最大的版本。

2. Go 代码示例

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 的去重也是后台异步进行的:

sql
-- 手动触发合并(生产环境慎用)
OPTIMIZE TABLE block_tasks FINAL;

2. 实时精确查询的替代方案

如果业务要求100%实时精确,建议使用聚合查询替代 FINAL

sql
-- 方法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;

最佳实践总结

  1. 版本字段不入 ORDER BYReplacingMergeTree(version_field) 中的版本字段不应出现在 ORDER BY 子句中

  2. ORDER BY 定义去重粒度ORDER BY 字段的组合决定了什么算是"重复数据"

  3. 高基数字段在前:在 ORDER BY 中将高基数字段(如ID、时间戳)放在前面

  4. 接受最终一致性ReplacingMergeTree 适合能接受短暂数据延迟的场景

  5. 实时需求用聚合:需要实时精确去重时,使用聚合查询而非 FINAL

结论

ReplacingMergeTree 的去重失效通常不是因为 FINAL 关键字的问题,而是由于表结构设计不当。记住这个简单的原则:版本字段决定保留谁,ORDER BY 字段决定谁是重复。正确区分这两者的角色,就能避免这个常见的陷阱。

通过本文的解决方案,你的 FINAL 查询将能够正确工作,返回真正去重后的数据结果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/923018.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

js中?? 和 || 的区别详解

?? 和 || 的区别详解 在 JavaScript/TypeScript 中,??(空值合并运算符)和 ||(逻辑或运算符)都用于提供默认值,但它们在处理不同值时有关键区别。 核心区别运算符 名称 触发条件 处理假值的方式?? 空值合并…

微信机器人API接口| 个人开发者必备

微信机器人API接口| 个人开发者必备 微信二次开发个人号api个人微信机器人开发api接口,微信个人号开发API在线接待更高效 在线沟通更快速、更有趣 语音回复 通过电脑端语音回复客户,提高效率 文件传输 支持文字、图片…

直击现场! “ 直通乌镇 ”开源赛复赛收官,OpenCSG担任评委,十强藏着哪些产业机会?

2025年9月16日,备受瞩目的“直通乌镇”全球互联网开源模型应用赛复赛在杭州圆满落幕。浙江省经济和信息化厅相关领导及各界专家、参赛团队代表齐聚一堂,共同见证了这一激动人心的时刻。值得关注的是,今年大赛首次设…

Python 列表生成式、字典生成式与生成器表达式

1. 列表生成式 (List Comprehension) 语法:[expression for item in iterable if condition] 示例:1.基本示例 # 创建平方数列表 squares = [x**2 for x in range(5)] print(squares) # [0, 1, 4, 9, 16]# 创建偶数…

java 解析json字符串,获取特定的字段值,JsonObject

java 解析json字符串,获取特定的字段值,JsonObjectjava 解析json字符串,获取特定的字段值package com.example.core.mydemo.java3;import com.google.gson.Gson; import com.google.gson.JsonObject; import com.go…

python 批量提取txt数据中的值写入csv

我有一堆雨滴谱txt数据,第一行是时间就是2024-05-10 10:21:00这样的格式,第二行是值,第三行是空格,然后第四行又是2024-05-10 10:21:00,第五行是值,第六行是空格,这样循环往复。给我写一个批量提取这些值的pyth…

【读书笔记】架构整洁之道 P5-2 软件架构 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

嘉兴 企业网站 哪家厦门网站建设模板

效率工具 推荐一个程序员的常用工具网站,效率加倍嘎嘎好用:程序员常用工具 云服务器 云服务器限时免费领:轻量服务器2核4G腾讯云:2核2G4M云服务器新老同享99元/年,续费同价阿里云:2核2G3M的ECS服务器只需99…

网站建设后台 手工上传网页图片显示不出来打叉

介绍: JavaScript是一种基于对象和事件驱动的编程语言,在Web开发中占据着重要的地位。随着前端技术的不断发展,出现了一系列的框架和库,Vue和React是其中较为知名的两个。 Vue是一个轻量级的JavaScript框架,由尤雨溪…

金华市金东区建设局网站上海软件外包公司有哪些

1.概述 QwtPlotMarker类是Qwt绘图库中用于在图表上绘制标记的类。标记可以是垂直或水平线、直线、文本或箭头等。它可用于标记某个特定的位置、绘制参考线或注释信息。 以下是类继承关系图: 2.常用方法 设置标记的坐标。传入x和y坐标值,标记将被放置在…

怎么做网站教程html文本文档wordpress 首页 缩略图

目录 一、网络文件 1.1.存储类型 1.2.FTP 文件传输协议 1.3.传输模式 二、内网搭建yum仓库 一、网络文件 1.1.存储类型 直连式存储:Direct-Attached Storage,简称DAS 存储区域网络:Storage Area Network,简称SAN&#xff0…

回忆中学的函数

这篇文章,带你一次性回顾中学时代里的那些函数。如果对初中、高中的函数还记忆模糊,建议往下翻一翻。 目录一、函数的意义要素特征二、初阶函数1. 一次函数函数特征应用示例2. 反比例函数函数特征应用示例3. 二次函数…

Java 一行一行的读取文本,小Demo 大学问

String str="A\n" +"B\n" +"C";在Java中,有多种方式可以一行一行地读取文本。以下是几种常用的方法: 1. 使用 BufferedReader + FileReader String str = "A\n" + "B\…

免费网站系统沧州讯呗网络科技有限公司

动态标签foreach,做过批量操作,但是foreach只能处理记录数不多的批量操作,数据量大了后,先不说效率,能不能成功操作都是问题,所以这里讲一讲Mybatis正确的批量操作方法: 在获取opensession对象…

数字化转型业务流程总览图

数字化转型业务流程总览图flowchart TDA[客户询价/委托] --> B[智能报价系统<br/>AI-Powered Quotation]B --> C{报价确认?}C -->|是| D[订单管理<br/>Order Management]C -->|否| E[报价调整…

MYSQL数据库取消表的约束

要修改MySQL中的chk_quantity约束以允许负数,可以通过以下步骤实现: 1. 删除原有约束 首先需要删除现有的chk_quantity约束: sqlCopy Code ALTER TABLE 表名 DROP CONSTRAINT chk_quantity; 2. 重新添加允许负数的…

家里wifi电信出口ip如何控制不变,解决访问云服务器上面的资源

家里wifi电信出口ip如何控制不变,解决访问云服务器上面的资源家里wifi电信出口ip如何控制不变,解决访问云服务器上面的资源 解决方案:通过在公司部署一台公共机器,通过远程的方式来连接,而公司的公共机器是可以将公…

2025 年京东 e 卡回收平台最新推荐排行榜:权威测评实时结算平台,助力用户安全高效转让京东 e 卡

随着数字消费的普及,京东 e 卡作为常用电商消费凭证,其闲置回收需求持续攀升。但当前回收市场乱象丛生,部分平台结算周期长达数天,严重影响用户资金周转;还有平台暗藏手续费,导致用户实际收益大幅缩水,更有非正…

【qml-12】Quick3D达成机器人鼠标拖拽转换视角(无限角度)与滚轮缩放

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025 年挤压造粒机源头厂家最新推荐榜单:前五企业技术实力、服务能力及口碑测评指南对辊挤压/化肥挤压/干粉挤压造粒机厂家推荐

随着有机肥产业朝着规模化、精细化方向快速发展,挤压造粒机作为生产核心设备,其质量与性能直接决定企业生产效率、产品品质及综合成本。但当前市场环境中,设备乱象频发:部分设备无法适配湿度 20%-40% 的发酵有机物…