ClickHouse内幕(1)数据存储与过滤机制

本文主要讲述ClickHouse中的数据存储结构,包括文件组织结构和索引结构,以及建立在其基础上的数据过滤机制,从Part裁剪到Mark裁剪,最后到基于SIMD的行过滤机制。

数据过滤机制实质上是构建在数据存储格式之上的算法,所以在介绍过滤机制前先介绍下ClickHouse中数据存储格式。

PS:本文基于ClickHouse v24.1

一、数据存储的目录结构

ClickHouse数据存储在目录结构上采用大数据系统常见的分区,然后分区内在进行细分文件的方式。

表中的数据首先按照分区键被划分为多个分区,分区键常采用日期的方式,比如下图中按照月份分区。每次数据批量插入形成一个最小的存储单元,ClickHouse中称为Part,Part归属于某一个分区。

ClickHouse中一个表的所有分区的所有Part放在同一个目录中,Part所属的分区可以根据Part名进行区分,Part的命名方式如下:

partition-id _ min-id _ max-id _ level

Part目录内部文件组织形式如下:

primary.idx - 是主键索引文件,记录了每个Mark对应的主键索引值,整个数据集一个.idx文件

[Column].mrk - 记录Mark(Mark在索引结构中介绍)对应的数据在数据文件(.bin文件)中的offset,用于根据Mark定为到数据位置,每个列一个.mrk文件。

[Column].bin - 真实数据文件,每个列一个.bin文件

checksums.txt - part checksum文件,用于校验数据完整性

columns.txt - 元数据,记录列名以及数据类型

count.txt - 元数据,记录该Part总行数,可以用于加速count(*)查询

partition.dat - 分区表达式

minmax_[Column].idx - 某一个列的最大最小值,可以用于加速查询,分区键固定有一个最大最小索引,用于分区裁剪

statistics_(column_name).stat - 列的统计信息,用于查询加速

二、索引结构

每个part形成一个完整的索引结构,整体上Clickhouse的存储是列式的,每个列单独存储(compact模式将多个文件合并成了一个,但本质上是一样的)。Clickhouse索引的大致思路是:首先根据索引列将整个数据集进行排序,这点类似MySQL的联合索引;其次将排序后的数据每隔8192行选取出一行,记录其索引值和序号,并形成稀疏索引,这个序号在Clickhouse中序号被称作Mark,也就是说Mark表示一组数据。

下图是一个二维表(date, city, action)的索引结构,其中(date,city)是索引列,整个part文件的宏观结构如下:

那么查询如何使用索引呢?以下查询为例:

select count(distinct action) where date=toDate(2020-01-01) and city=’bj’

1.查找primary.idx并找到对应的Mark集合(即数据block集合)

2. 对于要读取的每个列根据.mrk文件定位到Mark对应在数据文件.bin中的数据offset

3.读取到对应的数据,供后续计算

以上为宏观步骤,下面会介绍其具体原理。

三、何时使用索引

在SQL编译阶段,初始化执行Pipeline的时候,ClickHouse会分析待查询的数据,目标是解析出需要读取哪些Part的哪些Mark列表。解析结果如下所示的AnalysisResult。在真正执行的时候,Scan算子直接读取这些Mark列表对应的数据。

    struct AnalysisResult
    {
        RangesInDataParts parts_with_ranges;	// 最终要扫描的Part列表,以及每个Part的扫描范围(range)
        Names column_names_to_read;	                     // 需要读取那些列
        
        // 以下是一些统计信息	
        UInt64 total_parts = 0;		// Parts总数
        UInt64 selected_parts = 0;		// 命中的Part数量
        UInt64 selected_ranges = 0;		// 命中的range数量
        UInt64 selected_marks = 0;		// 命中的marks总数
        UInt64 selected_rows = 0;		// 命中的marks包含的总数据行数
    };

关键堆栈:

ReadFromMergeTree::selectRangesToRead
ReadFromMergeTree::getAnalysisResult()
ReadFromMergeTree::initializePipeline
QueryPlan::buildQueryPipeline
InterpreterSelectWithUnionQuery::execute() 
executeQuery

四、KeyCondition原理

在介绍如何使用索引前,先介绍下其核心算法。KeyCondition对外提供的语义是检查一个范围矩阵是否可以满足过滤条件。首先在构建KeyCondition的时候有两个必要参数,一个是过滤条件语法树这是过滤的主体,另一个是keys表示在判断是否满足的时候只关心这些列。然后当使用KeyCondition的时候,用户需要传入一个keys范围矩阵,来判断该范围矩阵是否满足过滤条件分。

范围矩阵是keys的范围数组,比如:c1,c2,c3三个主键列构成的一个范围矩阵可以是[[a, b], [1, 1], [x, x]],当用户进行Mark裁剪的时候,会生成范围矩阵然后判断该范围矩阵是否满足条件。又比如常用的时间分区构成的一个范围矩阵可以是[[2023-11-22, 2023-11-22]],当用户进行分区裁剪的时候,遍历每个分区构建范围矩阵,然后判断该分区是否满足条件。

满足过滤条件分三种情况,1范围矩阵中的数据全部满足过滤条件,2部分满足,3全部不满足。为表示以上三个语义ClickHouse设置了一个数据结构 {can_be_true, can_be_false},它的{true, false}、{true, true}、{false, true}三个值对应以上三种语义。

KeyCondition的核心是一个RPN表达式,主要流程分成两个阶段,1构建RPN,即将过滤条件转换成RPN表达式;2匹配,匹配一个范围矩阵是否满足条件。RPN的优势是表达一个表达式的时候不需要括号,在进行计算的时候使用栈,在碰到操作符的时候从栈顶弹出操作数就可以完成整个表达式的计算。



通过类比能够更好的理解在过滤场景中如何使用RPN,上图展示了RPN在四则运算法则和过滤两个场景中的使用情况,另种场景中唯一的区别是结果类型不一样,一个是number,一个是BoolMask {can_be_true, can_be_false}。更多关于RPN请参考wiki。

四、如何使用索引

1. Part裁剪

Part裁剪的目的是一次性过滤整个Part文件,只保留可能包含目标数据的Part。ClickHouse中的Part裁剪过程中会构建两个KeyCondition。其中一个被称为,主要用于根据分区列的最大值和最小值进行Part过滤。minmax_idx_condition的keys为构成分区键对应的原始列,比如分区键为toDate(date_time),那么它的key为date_time列,后续进行过滤的时候传入的也是date_time对应的值组成的范围矩阵。下图展示了minmax_idx_condition的工作原理。



另一个被称为,主要用于过滤分区。partition_pruner的key为分区表达式组成的列,比如分区键为toDate(date_time),那么它的key为toDate(date_time),后续进行过滤的时候传入的是分区对应的值组成的范围矩阵。下图展示了partition_pruner的工作原理。

Part裁剪的主要逻辑位于:MergeTreeDataSelectExecutor::selectPartsToRead,大家可以自行参阅:

for (size_t i = 0; i < prev_parts.size(); ++i)
{
           if (!minmax_idx_condition->checkInHyperrectangle(        // 基于分区键的列的最大最小值索引,进行Part裁剪
                part->minmax_idx->hyperrectangle, minmax_columns_types).can_be_true)
            continue;

            if (partition_pruner->canBePruned(*part))	                   // 基于分区表达式,进行Part裁剪
                continue;

            parts.push_back(prev_parts[i]);
}

2. Mark裁剪

过滤完Part后会进一步进行Mark过滤,首先回顾下什么是Mark,在一个Part内部数据按照主键进行排序,然后每隔8192行数据将数据进行分组,这个分组id就被称为Mark,Mark表示一组按照主键有序的数据。

ClickHouse索引过滤的最小粒度就是Mark。下图展示了c1,c2,c3三个列组成主键的一个Part的数据结构以及Mark过滤的流程。

在Mark裁剪过程中输入的是一个MarkRange,比如:[2, 5],输出的是一系列的MarkRange,那么如何使用KeyCondition呢?

因为KeyCondition进行判断的时候输入的是主键列的范围矩阵,所以需要先将MarkRange转换成范围矩阵。将MarkRange转换成范围矩阵,相当于将多维空间的范围转换为一维空间的范围,所以转换出来的范围矩阵有很多个,这里不做过多讨论,有兴趣可以参考:方法。MarkRange [2, 5]转换成的一个范围矩阵为:[[a, a], [1, 1], [z, +inf]],下图展示了Mark裁剪的工作原理:

至此我们过滤出来了一系列的mark,同时数据存储结构和索引在过滤过程中的使命也完成了。接下来需要过滤每个mark中的Row。

六、Row过滤

Row过滤发生在查询执行阶段。ClickHouse中Row过滤也是按照batch的方式进行,该batch在ClickHouse内部被称为Chunk,它大小可能与Mark不同。ColumnVector::filter记录了数值类型的列过滤的主要逻辑(其它列过滤思路大同小异)。

1. 生成Filter Mask:

首先对根据过滤条件给一个Chunk的数据生成一个Filter Mask,Filter Mask是一个行数等于Chunk大小的UInt8数组,数组中只有2个值,其中0表示该行的数据不符合过滤条件,1表示符合。

2. Filter column by Filter Mask:

根据Filter Mask进行数据过滤,过滤的过程中使用到了SIMD技术进行优化,具体流程如下:

首先将数据按照64个的粒度为一组,按照组的粒度进行处理,然后将byte mask转换成bit mask,参考函数:,转换后的bit mask可以直接用一个UInt64表示。

对于每个分组按照数据分布的不同情况分别进行过滤

1)如果bit_mask等于0,也就是该分组的bit mask全0,直接忽略该分组

2)如果bit mask 全1,将整个分组复制到结果集中

3)[0]+[1]+或者[1]+[0]+,将byte maskt中连续1的区间复制到结果集





3. Filter column by Filter Mask version 2

过滤部分ClickHouse提供了一个SIMD的版本(使用AVX512指令集)。依然将数据按照64个的粒度为一组,按照组的粒度进行处理,然后将byte mask转换成bit mask,对于每个分组按照数据分布的不同情况分别处理

1)如果bit mask 全0,直接忽略该分组

2)bit mask 全1,将整个分组复制到结果集中,使用向量化方法批量进行复制

_mm512_storeu_si512(reinterpret_cast(&res_data[current_offset + i]), 
               _mm512_loadu_si512(reinterpret_cast(data_pos + i)));

3)其它情况,使用向量化函数,根据mask将数据拷贝到结果集中

4. 过滤结尾部分



5. Row过滤优化总结

1 为什么使用Filter Mask?

1)过滤多个列的时候可以复用Filter Mask;2)便于使用SIMD指令

2 为什么分组处理?

因为ClickHouse数据按照主键排序,很多情况下数据按照某些列局部有序,过滤的时候可将整个分组过滤掉或者命中的概率比较大,这样相比单个行的处理方式,在处理过程中没有判断,消除了分支预测失败的场景,避免CPU执行流水线被破坏,同时增大指令与数据缓存的命中率,这也是过滤高性能的核心设计点。

3 是不是使用位宽越大的SIMD指令集越好,比如AVX512一定快于SSE2?

大多数场景下是的,但是在过滤场景中不是的。因为增大位宽,那么分组大小也会相应增大,分组越大全零或者全一的概率越小,那么过滤整个分组的概率越小,所以需要看数据分布情况。



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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

GNN与Transformer创新结合!模型性能起飞!

前言 近年来&#xff0c;图神经网络&#xff08;GNN&#xff09;和Transformer模型分别凭借其独到的优势&#xff0c;在处理复杂数据结构和识别序列间的相互依赖性方面取得了突破性进展&#xff0c;这些优势使得GNN和Transformer的结合成为图表示学习领域的一个有前景的研究方…

微信小程序实现图生图(AI动漫特效)效果代码(触站API)

1.效果 触站AI图生图 2.本次用的是触站平台的API,我申请的适用积分,有水印(博主没钱)。如果需要没有水印的可以去买他们的资源包 3.首先我们需要去触站官网平台注册/登录账号(已注册可跳过该步骤) 4.开通API权限 我们可以在主页看到自己免费获取的500积分,用于接口调用…

Echarts 中type是value的X轴在设置了interval间隔后没有展示

文章目录 问题分析问题 Echarts中type是value的X轴在设置了interval间隔后没有展示 分析 之前代码是这样写的:axisLabel 属性中设置了 interval ,但未起作用,原因如下 在 ECharts 中,interval 属性是用于类目型(category)轴的刻度间隔设置,并不适用于数值型(value)…

2024会声会影激活码免费注册码大揭秘!

在当今数字化时代&#xff0c;视频编辑已经成为了许多人日常生活和工作中不可或缺的一部分。无论是制作短视频、Vlog还是专业影视剪辑&#xff0c;一款优秀的视频编辑软件都能让我们事半功倍。而市面上众多的视频编辑软件中&#xff0c;会声会影无疑是备受瞩目的一款。本文将为…

如何将华为Ascend手机的短信和联系人安全传输到电脑

华为Ascend系列手机以其流畅的使用体验、光滑的触感以及轻巧的设计赢得了市场的青睐。不仅如此&#xff0c;Ascend系列手机还以亲民的价格和出色的用户体验&#xff0c;搭载了众多先进功能&#xff0c;如Ascend P6的4.7英寸大屏、海思四核处理器、2GB RAM和800万像素摄像头等。…

3 - 大的国家(高频 SQL 50 题基础版)

3.大的国家 -- 查询属性&#xff1a;国家名称、人口和面积 select name,population,area fromWorld where area>3000000 OR population>25000000;

【启明智显分享】Model3A 7寸彩屏应用于智能中控屏解决方案

比尔盖茨曾在出版的《未来之路》中预言&#xff1a;“在不远的未来&#xff0c;没有智能家居系统的住宅会像不能上网的住宅一样不合潮流”。随着5G时代的到来&#xff0c;国内十几万家企业竞逐智能家居的产业赛道。智能家居的风口已然到来&#xff0c;智能家居产品也不断进行升…

图片如何修改尺寸?四种好用的修改图片尺寸方法!

图片如何修改尺寸&#xff1f;图片是一种常见的文件类型&#xff0c;它存在于什么生活的方方面面&#xff0c;虽然图片很好用&#xff0c;但是大家日常也要注意图片的尺寸&#xff0c;如果图片尺寸不对是会带来很多问题的&#xff0c;下面小编就举例说明几个问题&#xff0c;首…

Android Studio 中文汉化教程

1. 中文语言包 一般jetbrains系列软件都可以使用“中文语言包”进行汉化&#xff0c;语言包如下图所示&#xff1a; 然而&#xff0c;Android Studio的Marketplace并没有类似的中文语言包&#xff08;如下图&#xff09;&#xff0c;经过查阅相关资料发现需要去jetbrains的插件…

【C语言】结构体(及位段)

你好&#xff01;感谢支持孔乙己的新作&#xff0c;本文就结构体与大家分析我的思路。 希望能大佬们多多纠正及支持 &#xff01;&#xff01;&#xff01; 个人主页&#xff1a;爱摸鱼的孔乙己-CSDN博客 欢迎 互粉哦&#x1f648;&#x1f648;&#xff01; 目录 1. 声明结构…

查看 samba 文件共享服务器地址的具体 IP

问题背景 在某个局域网中&#xff0c;已知 samba 文件共享服务器的地址如 \\samba_share在该局域网的子网中&#xff0c;由于 dns 服务器缺失&#xff0c;无法通过地址 \\samba_share 直接访问该服务器 解决方法 使用 ping 命令查看某个地址的 ip &#xff1a; ping [addre…

【Java面试】十四、LinkedList相关

文章目录 1、单向链表1.1 结构1.2 查询的时间复杂度1.3 插入删除的时间复杂度 2、双向链表2.1 时间复杂度 3、ArrayList和LinkedList的区别是什么 1、单向链表 1.1 结构 存储空间上&#xff0c;非连续链表的每个元素称结点Node每个结点包括两块&#xff1a;存储数据的数据域d…

C++11标准-详解

目录 1、列表初始化 2、隐式类型转换 1&#xff09;概念理解 2&#xff09;举例增进理解 3&#xff09;隐式与显式区别&#xff1f; a、直接初始化 vs 拷贝初始化 b、构造函数调用 c、语义上的差异 d、性能差异 4&#xff09;explicit 关键字 5&#xff09;多参数的隐…

Python 组合序号

import pandas as pd # 创建一个示例数据框 data { group: [A, A, A, B, B, C, C, C, C], value: [3, 1, 2, 5, 4, 6, 9, 7, 8] } df pd.DataFrame(data) # 先按group分组&#xff0c;再按value列升序排序 df_sorted_asc df.sort_values(by[group, value]) # 使…

Ansible自动化运维工具 playbook 剧本

一、Playbooks 1. playbooks 介绍 Playbooks&#xff08;剧本&#xff09;是一种用于定义自动化任务的文件&#xff0c;通常与诸如Ansible等工具相关联。它们以YAML格式编写&#xff0c;包含了一系列有组织的任务&#xff0c;这些任务可以在远程计算机上执行。一个Playbook通…

如果小孩对什么也不感兴趣,只爱看手机和电脑 怎么办?怎样培养孩子兴趣?兴趣有哪些可以选择?如何快速找到定位小孩的兴趣?

自古到今&#xff0c;生存都很艰难&#xff01;不论是动物还是人 得看小孩兴趣&#xff0c;有些爱读书&#xff0c;有些爱踢球&#xff0c;就怕啥也不爱&#xff0c;只看手机和电脑的&#xff0c;想卷都没个方向。 特长&#xff1a;是你从60亿人中不一样的地方。突出才易于生…

深度学习笔记:2.Jupyter Notebook

Jupyter Notebook 常用操作快捷键魔法指令_jupyter notebook快捷键调用函数-CSDN博客https://blog.csdn.net/qq_26917905/article/details/137211336?ops_request_misc%257B%2522request%255Fid%2522%253A%2522171748112816800182160793%2522%252C%2522scm%2522%253A%25222014…

模型测试优化

针对怼螺丝孔场景交叉测试 文章目录 修改一&#xff1a;修改二&#xff1a; 基于训练场景&#xff0c;进行修改&#xff0c;用以验证泛化性 模型说明&#xff1a;训练所用的物体模型上&#xff0c;有两个孔位&#xff0c;其中左侧为1号孔位&#xff0c;右侧为2号孔位 现状&…

Scalable Diffusion Models with Transformers

Metahttps://github.com/facebookresearch/DiT/tree/main?tabreadme-ov-file 问题引入 transformer架构的latent diffusion model&#xff0c;有较好的延展性并是sota&#xff1b; methods patchify&#xff1a;原图片 I ∈ R H W 3 I\in\mathbb{R}^{H\times W\times 3…

python运算符和表达式

目录 算数运算符 赋值运算符 关系运算符 逻辑运算符 位运算符 成员运算符 运算符优先级 易错点&#xff1a; 算数运算符 赋值运算符 关系运算符 int可以转换成float 逻辑运算符 可以是一个运算也可以是一个字符串 左边为空格&#xff0c;为假&#xff0c;输出为空 优…