实战攻略 | ClickHouse优化之FINAL查询加速
【本文作者:擎创科技资深研发 禹鼎侯】
查询时为什么要加FINAL
我们在使用ClickHouse存储数据时,通常会有一些去重的需求,这时候我们可以使用ReplacingMergeTree引擎。这个引擎允许你存储重复数据,但是在merge的时候会根据order by的字段进行去重。
它的去重逻辑是使用新数据覆盖旧数据。
但是很多时候,merge并不是实时的,他可能会在数据插入后几分钟甚至十几分钟后才会进行,而我们对数据的查询却往往却是实时的。这个时候就必然出现一个问题:
那些还没有来得及merge的数据,查询出来会有重复。这与我们所期望的效果是背道而驰的。
事实上,即便是merge发生了,我们也不能保证数据一定没有重复。我们举个简单的例子:
如上图所示:p1~p4是四个原始part,发生合并之后数据进行了去重,最终合并成了p1_4, 对id去重之后有四个值,分别为1,2,3,4, 假设此时又有一个新part p5插入,此时去查询仍然能搜到两条id为2和3数据。
因此,即使发生了合并,我们也不能保证数据就一定是唯一的。
ClickHouse为了解决这个问题,提供了FINAL语法,从字面意义上理解,就是返回merge最终态的数据结果。它的效果与OPTIMIZE FINAL 是一致的。
不过,SELECT FINAL仅是在读时合并,并不会实际将底层数据合并,而OPTIMIZE FINAL则是实实在在的发生合并,这是二者的本质的区别。
FINAL查询会有什么问题
那么大家肯定也已经发现了。这种加了FINAL的查询,性能会有很大的问题!
因为FINAL相当于在查询时执行一次OPTIMIZE FINAL,而这个操作本质上是将同一个partition内的数据合并成一个part。在数据量不大的情况下还好,这个操作还能很快返回,如果数据集比较大,比如单个分区的数据已经达到了上千万级,甚至上亿级,那么一次FINAL可真是要了亲命了。
为了让大家直观的感受到这种性能差距,我们来举个例子。
我们使用ClickHouse官方提供的压测数据来进行举例:
https://clickhouse.com/docs/en/getting-started/example-datasets/opensky#validate-data
建表语句如下:
CREATE TABLE opensky
(
`callsign` String,
`number` String,
`icao24` String,
`registration` String,
`typecode` String,
`origin` String,
`destination` String,
`firstseen` DateTime,
`lastseen` DateTime,
`day` DateTime,
`latitude_1` Float64,
`longitude_1` Float64,
`altitude_1` Float64,
`latitude_2` Float64,
`longitude_2` Float64,
`altitude_2` Float64
)
ENGINE = ReplacingMergeTree
PARTITION BY toYYYYMMDD(day)
ORDER BY (origin, destination, callsign)
我们将数据导入两遍,这样就一定会得到重复的数据:
ck94 :) select count() from opensky;
SELECT count()
FROM opensky
Query id: 0d65affc-873e-4847-9ee0-ae749b091bd1
┌───count()─┐
│ 127132244 │
└───────────┘
1 row in set. Elapsed: 0.007 sec.
接下来我们来执行一个查询语句:
ck94 :) select avg(altitude_1) from opensky where day >= '2020-10-01 00:00:00' and typecode = 'B737';
SELECT avg(altitude_1)
FROM opensky
WHERE (day >= '2020-10-01 00:00:00') AND (typecode = 'B737')
Query id: bcb16bd6-91fc-4993-a4de-87e1731d7760
┌────avg(altitude_1)─┐
│ 1458.7475907858743 │
└────────────────────┘
1 row in set. Elapsed: 0.186 sec. Processed 34.74 million rows, 812.04 MB (186.88 million rows/s., 4.37 GB/s.)
Peak memory usage: 14.39 MiB.
可以看到,当我们不使用final时,上面这个sql仅仅使用了0.186秒就返回了结果。
接下来我们看看加了final的效果:
ck94 :) select avg(altitude_1) from opensky final where day >= '2020-10-01 00:00:00' and typecode = 'B737';
SELECT avg(altitude_1)
FROM opensky
FINAL
WHERE (day >= '2020-10-01 00:00:00') AND (typecode = 'B737')
Query id: 63f760a6-9bca-4a40-a7b3-237eacae356a
┌────avg(altitude_1)─┐
│ 1336.4280164372838 │
└────────────────────┘
1 row in set. Elapsed: 12.784 sec. Processed 48.60 million rows, 3.06 GB (3.80 million rows/s., 239.37 MB/s.)
Peak memory usage: 4.16 GiB.
上面这个SQL足足耗费了12.784秒!这可是近70倍的差距,我们当前的数据集还不算大,如果数据集再大一点,那么这个查询性能慢的问题将无限扩大化,最终影响到生产使用。
这让老夫如何是好
那么,这个问题如此明显,有没有什么好的优化手段呢?
自然是有的。
接下来就来介绍两种方法来做final查询的优化。
优化手段1:使用PREWHERE
所谓PREWHERE,就是在查询之前,先将数据过滤掉一部分,这样,目标数据集的规模小了,执行FINAL自然会快了。
我们使用EXPLAIN SYNTAX 执行计划来看一下为啥第一条不加FINAL的SQL快得离谱:
EXPLAIN SYNTAX
SELECT avg(altitude_1)
FROM opensky
WHERE (day >= '2020-10-01 00:00:00') AND (typecode = 'B737')
Query id: 046a721e-7690-4d0b-9457-20e825d0e152
┌─explain─────────────────────────────────────────────────────────┐
│ SELECT avg(altitude_1) │
│ FROM opensky │
│ PREWHERE (day >= '2020-10-01 00:00:00') AND (typecode = 'B737') │
└─────────────────────────────────────────────────────────────────┘
3 rows in set. Elapsed: 0.003 sec.
可以看到,上面这条SQL,实际上clickhouse优化器已经对其进行改写成了PREWHERE。
我们再看看加了FINAL的执行计划:
EXPLAIN SYNTAX
SELECT avg(altitude_1)
FROM opensky
FINAL
WHERE (day >= '2020-10-01 00:00:00') AND (typecode = 'B737')
Query id: c4ae0a63-7a55-4216-815a-25ff44ee538e
┌─explain──────────────────────────────────────────────────────┐
│ SELECT avg(altitude_1) │
│ FROM opensky │
│ FINAL │
│ WHERE (day >= '2020-10-01 00:00:00') AND (typecode = 'B737') │
└──────────────────────────────────────────────────────────────┘
那就离了个大谱了!
为神马就加了个FINAL,他就不能使用PREWHERE优化了?
按照官方的说法: 如果查询条件里带有主键字段,clickhouse会默认会使用PREWHERE进行优化,可以提前减少数据集的大小,一来避免过多内存造成OOM,二来自然是可以加速查询了。
然而十分悲催的是,这个优化对加了FINAL的查询不会生效。
那么我们怎么能让它使用到PREWHERE优化呢?
这里提供两种方法:
一种是显式指定。
ck94 :) select avg(altitude_1) from opensky final prewhere day >= '2020-10-01 00:00:00' and typecode = 'B737';
SELECT avg(altitude_1)
FROM opensky
FINAL
PREWHERE (day >= '2020-10-01 00:00:00') AND (typecode = 'B737')
Query id: ce72c7a9-38a1-4ae8-957e-d951e0dd9d0b
┌────avg(altitude_1)─┐
│ 1424.1051732278174 │
└────────────────────┘
1 row in set. Elapsed: 1.429 sec. Processed 48.60 million rows, 3.06 GB (34.01 million rows/s., 2.14 GB/s.)
Peak memory usage: 1.19 GiB.
可以看到,我们改成显式使用PREWHERE之后,查询性能立马减少到了1.429秒,差不多有9倍左右的提升,可见这个提升是巨大的。
第二是利用子查询命中PREWHERE。
那么,有木有什么办法不显式指定PREWHERE,但是让其有这个优化呢?而且我们并不能保证所有的条件都会带上主键索引。
前面介绍过,当查询条件带主键,且不加final的时候,clickhouse会默认使用PREWHERE进行优化,那么我们是不是可以先用子查询命中PREWHERE,然后再final呢?
我们将SQL改成如下样子:
ck94 :) select avg(altitude_1) from opensky where (origin, destination, callsign) in (select origin, destination, callsign from opensky where day >= '2020-10-01 00:00:00' and typecode = 'B737') and day >= '2020-10-01 00:00:00' and typecode = 'B737';
SELECT avg(altitude_1)
FROM opensky
WHERE ((origin, destination, callsign) IN (
SELECT
origin,
destination,
callsign
FROM opensky
WHERE (day >= '2020-10-01 00:00:00') AND (typecode = 'B737')
)) AND (day >= '2020-10-01 00:00:00') AND (typecode = 'B737')
Query id: 61ae0590-de1a-44b1-866f-0c54a919c762
┌────avg(altitude_1)─┐
│ 1458.7475907858748 │
└────────────────────┘
1 row in set. Elapsed: 0.768 sec. Processed 69.47 million rows, 4.09 GB (90.43 million rows/s., 5.32 GB/s.)
Peak memory usage: 69.15 MiB.
耗时降到了0.768秒,这已经非常接近不加final的裸查询了。牛逼class!
优化手段2:禁用final查询跨分区merge
ClickHouse本身是允许跨分区进行数据替换的,这无疑复杂化了FINAL查询的逻辑。但事实上,只要我们的数据分区合理,这种情况完全可以规避掉。这时候我们可以使用do_not_merge_across_partitions_select_final=1这个配置来禁用跨final查询时分区合并。这意味着clickhouse仅在本分区内进行merge去重。
我们来看看效果:
ck94 :) select avg(altitude_1) from opensky final where day >= '2020-10-01 00:00:00' and typecode = 'B737' SETTINGS do_not_merge_across_partitions_select_final=1;
SELECT avg(altitude_1)
FROM opensky
FINAL
WHERE (day >= '2020-10-01 00:00:00') AND (typecode = 'B737')
SETTINGS do_not_merge_across_partitions_select_final = 1
Query id: 41aa4fc9-5933-43e1-86bf-a374eab15dd9
┌────avg(altitude_1)─┐
│ 1458.6043678101414 │
└────────────────────┘
1 row in set. Elapsed: 0.732 sec. Processed 34.74 million rows, 2.18 GB (47.44 million rows/s., 2.98 GB/s.)
Peak memory usage: 2.86 GiB.
仅耗时0.732秒,同样牛的一批。
事实上,我们建议在部署集群时,将这个配置作为默认配置进行设置,在知名项目clickhouse-operator 中,这个配置就是默认打开的。
那么,综合上面两种优化手段,这就是最终形态了:
ck94 :) select avg(altitude_1) from opensky where (origin, destination, callsign) in (select origin, destination, callsign from opensky where day >= '2020-10-01 00:00:00' and typecode = 'B737') and day >= '2020-10-01 00:00:00' and typecode = 'B737' SETTINGS do_not_merge_across_partitions_select_final = 1;
SELECT avg(altitude_1)
FROM opensky
WHERE ((origin, destination, callsign) IN (
SELECT
origin,
destination,
callsign
FROM opensky
WHERE (day >= '2020-10-01 00:00:00') AND (typecode = 'B737')
)) AND (day >= '2020-10-01 00:00:00') AND (typecode = 'B737')
SETTINGS do_not_merge_across_partitions_select_final = 1
Query id: 298c3d92-67da-4379-8e1d-43f85c99cc2c
┌────avg(altitude_1)─┐
│ 1458.7475907858745 │
└────────────────────┘
1 row in set. Elapsed: 0.678 sec. Processed 69.47 million rows, 4.09 GB (102.45 million rows/s., 6.03 GB/s.)
Peak memory usage: 69.89 MiB.
虽然还是比不加final的要慢一丢丢,但总体上已经到了可以接受的程度了。
———— THE END ————
相关文章:

实战攻略 | ClickHouse优化之FINAL查询加速
【本文作者:擎创科技资深研发 禹鼎侯】 查询时为什么要加FINAL 我们在使用ClickHouse存储数据时,通常会有一些去重的需求,这时候我们可以使用ReplacingMergeTree引擎。这个引擎允许你存储重复数据,但是在merge的时候会根据order …...

5G NR gNB 逻辑架构及其功能拆分选项
5G NR gNB 逻辑架构及其功能拆分选项 中央单元 (CU) 和分布式单元功能拆分选项RAN 分体架构的优势在哪里使用哪个拆分函数?参考: 5G NR gNB Logical Architecture and It’s Functional Split OptionsCentral Unit (CU) and Distributed Unit Functional…...
PyQt入门指南四十六 性能优化策略
在PyQt应用程序中,性能优化是一个重要的考虑因素,尤其是在处理大型数据集或复杂图形界面时。以下是一些常见的性能优化策略: 1. 使用延迟加载(Lazy Loading) 延迟加载是一种优化技术,只在需要时加载资源。…...

【RMA】基于知识注入和模糊学习的多模态歧义分析
abstract 多模态情感分析(MSA)利用互补的多模态特征来预测情感极性,主要涉及语言、视觉和音频三种模态。现有的多模态融合方法主要考虑不同模态的互补性,而忽略了模态之间的冲突所导致的歧义(即文本模态预测积极情绪&…...

CulturalBench :一个旨在评估大型语言模型在全球不同文化背景下知识掌握情况的基准测试数据集
2024-10-04,为了提升大型语言模型在不同文化背景下的实用性,华盛顿大学、艾伦人工智能研究所等机构联合创建了CulturalBench。这个数据集包含1,227个由人类编写和验证的问题,覆盖了包括被边缘化地区在内的45个全球区域。CulturalBench的推出&…...

Git 入门篇(一)
前言 操作系统:win11 64位 与gitee搭配使用 Git 入门篇(一) Git 入门篇(二) Git 入门篇(三) 目录 git下载、安装与配置 下载 安装 配置 git下载、安装与配置 下载 官网:git-…...
一个灵活且功能强大的动画库 Popmotion
一个灵活且功能强大的动画库 Popmotion 什么是 Popmotion? Popmotion 是一个强大的 JavaScript 动画库,提供了一系列简洁的 API,方便开发者创建流畅的动画效果。它支持不同类型的动画,包括 CSS 动画、SVG 动画和 DOM 动画&#…...

如何解决传统能源企业后备人才不足、人才规划缺失问题
如何解决传统能源企业后备人才不足、人才规划缺失问题 很多传统能源企业都面临着老员工逐渐退休,新员工还没有培养起来的问题,缺乏提前对人力资源规划的意识,导致当企业要开展新业务时或者老员工离职的时候,缺乏合适的人选。特别…...

PDF模板制作与填充(Java)
1.PDF模板制作 准备原始模板 准备一个原始PDF模板,可以编辑好Word,预留出要填充的部分,再转换成PDF格式。 设置表单域 用任意PDF编辑器打开PDF模板文件,设置表单域,下面以WPS为例: 拖动文本域到需要填充的…...
LeetCode题练习与总结:迷你语法分析器--385
一、题目描述 给定一个字符串 s 表示一个整数嵌套列表,实现一个解析它的语法分析器并返回解析的结果 NestedInteger 。 列表中的每个元素只可能是整数或整数嵌套列表 示例 1: 输入:s "324", 输出:324 解释ÿ…...
Unity WebGL交互通信
Unity 调用 H5 本文使用的 unity 版本为:2021.3.3 1.在unity中通过c#的特性DllImport导出外部实现函数 [DllImport("__Internal")]private static extern void callJsString(string param);[DllImport("__Internal")]private static extern vo…...

王道考研之数据结构
数据结构系列 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 数据结构 数据结构系列1.线性表1.1 线性表的定义和相关概念1.2 线性表的创销 增删查改 判空表长打印 2.顺序表2.1 顺序表定义和相关概念2.2 顺序表的静态实现2.3 顺序表的…...

实习冲刺Day17
算法题 x的平方根 69. x 的平方根 - 力扣(LeetCode) class Solution { public:int mySqrt(int x) {long left 0,right x;//定义左右边界//数值取的大longlong类型while (left < right) {long mid (right-left1)/2left;//定义中间节点if ((mid *…...
我自己nodejs练手时常用的一些库基础用法
我自己在使用nodejs以及前端实战练习时常用的一些库的基本使用 1.bcrypt //注册账号时,给密码加密 password是前端传过来的密码,hashPassword是存到数据库中的密码 const bcrypt require(bcrypt) const hashPassword bcrypt.hash(password,10) //登…...

岛屿数量问题
给一个0 1矩阵,1代表是陆地,0代表海洋, 如果两个1相邻,那么这两个1属于同一个岛。我们只考虑上下左右为相邻。 岛屿问题: 相邻陆地可以组成一个岛屿(相邻:上下左右) 判断岛屿个数。 C 解决方案 #include &…...

智能制造基础- TPM(全面生产维护)
TPM 前言一、TPM二、TPM实施步骤三、 消除主要问题3.1 实施指南3.2 如何进行“主要问题”的消除? 四、自主维护4.1 实施指南4.2 主要工作内容4.3 如何进行“自主维护“ 五、计划维护5.1 实施指南5.2 如何实施计划维护 六、TPM 适当的 设备 设计5.1 实施指南5.2 如何…...
C++学习笔记----11、模块、头文件及各种主题(一)---- 模板概览与类模板(4)
2.2.2、显式实例化 有危险存在于有些类模板成员函数的编译错误,在隐式实例化时没有注意到。未被使用的类模板成员函数也可能包含语法错误,因为它们不会被编译到。这会使得检测代码的语法错误很困难。可以强制编译器生成所有成员函数的代码,vi…...

【力扣热题100】[Java版] 刷题笔记-160. 相交链表
题目:160. 相交链表 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。 图示两个链表在节点 c1 开始相交: 题目数据 保证 整个链式结构中不存在环。 注意…...

多线程和线程同步复习
多线程和线程同步复习 进程线程区别创建线程线程退出线程回收全局写法传参写法 线程分离线程同步同步方式 互斥锁互斥锁进行线程同步 死锁读写锁api细说读写锁进行线程同步 条件变量生产者消费者案例问题解答加强版生产者消费者 总结信号量信号量实现生产者消费者同步-->一个…...

贝式计算的 AI4S 观察:使用机器学习对世界进行感知与推演,最大魅力在于横向扩展的有效性
「传统研究方法高度依赖于科研人员自身的特征和问题定义能力,通常采用小数据,在泛化能力和拓展能力上存疑。而 AI 研究方法则需要引入大规模、高质量数据,并采用机器学习进行特征抽取,这使得产生的科研结果在真实世界的问题中非常…...
挑战杯推荐项目
“人工智能”创意赛 - 智能艺术创作助手:借助大模型技术,开发能根据用户输入的主题、风格等要求,生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用,帮助艺术家和创意爱好者激发创意、提高创作效率。 - 个性化梦境…...

【kafka】Golang实现分布式Masscan任务调度系统
要求: 输出两个程序,一个命令行程序(命令行参数用flag)和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽,然后将消息推送到kafka里面。 服务端程序: 从kafka消费者接收…...

家政维修平台实战20:权限设计
目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系,主要是分成几个表,用户表我们是记录用户的基础信息,包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题,不同的角色…...
unix/linux,sudo,其发展历程详细时间线、由来、历史背景
sudo 的诞生和演化,本身就是一部 Unix/Linux 系统管理哲学变迁的微缩史。来,让我们拨开时间的迷雾,一同探寻 sudo 那波澜壮阔(也颇为实用主义)的发展历程。 历史背景:su的时代与困境 ( 20 世纪 70 年代 - 80 年代初) 在 sudo 出现之前,Unix 系统管理员和需要特权操作的…...
是否存在路径(FIFOBB算法)
题目描述 一个具有 n 个顶点e条边的无向图,该图顶点的编号依次为0到n-1且不存在顶点与自身相连的边。请使用FIFOBB算法编写程序,确定是否存在从顶点 source到顶点 destination的路径。 输入 第一行两个整数,分别表示n 和 e 的值(1…...

优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?
Redis 的发布订阅(Pub/Sub)模式与专业的 MQ(Message Queue)如 Kafka、RabbitMQ 进行比较,核心的权衡点在于:简单与速度 vs. 可靠与功能。 下面我们详细展开对比。 Redis Pub/Sub 的核心特点 它是一个发后…...
Spring AI Chat Memory 实战指南:Local 与 JDBC 存储集成
一个面向 Java 开发者的 Sring-Ai 示例工程项目,该项目是一个 Spring AI 快速入门的样例工程项目,旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。 项目采用模块化设计,每个模块都专注于特定的功能领域,便于学习和…...
MySQL 主从同步异常处理
阅读原文:https://www.xiaozaoshu.top/articles/mysql-m-s-update-pk MySQL 做双主,遇到的这个错误: Could not execute Update_rows event on table ... Error_code: 1032是 MySQL 主从复制时的经典错误之一,通常表示ÿ…...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...