希音面试:说说 ClickHouse Group By 执行流程 ?CK 能支持 十亿级数据 实时分析的原理 是什么?

本文 的 原文 地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

尼恩说在前面

在45岁老架构师 尼恩的读者交流群(50+)中,通过 Java+AI双驱架构 帮助很多小伙伴拿到了一线企业如 字节、得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团、蚂蚁、得物的面试资格,遇到很多很重要的面试题:

说说 ClickHouse Group By 执行流程 ?

ClickHouse 支持 高吞吐写入低延迟查询,可支撑亿级乃至十亿级数据的实时分析,原因是什么?

为啥mysql 不行?

最近有小伙伴在 面试希音,遇到 到了这个的面试题。 小伙伴没有 回答好,到手到 大厂 offer就 这样飞走了。 非常可惜。

小伙伴面试完了之后,来求助尼恩。

那么,遇到 这个问题,该如何才能回答得很漂亮,才能 让面试官刮目相看、口水直流。

所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

一、ClickHouse 高性能分析型数据库核心解析

1、ClickHouse 概述

ClickHouse 核心优势是高吞吐写入低延迟查询,可支撑亿级乃至十亿级数据的实时分析需求。

ClickHouse 与其他数据工具的定位差异,可通过以下图 区分:

Mermaid

2、ClickHouse 整体架构

ClickHouse 的架构设计是其性能的核心基础,核心差异在于“存储与计算的关系”和“MPP 并行能力”。

ClickHouse 是面向联机分析处理(OLAP)场景的分布式列式数据库,其核心架构包含两大模块:

  • 存储层:负责数据持久化与压缩优化
  • 计算层:处理查询执行与分布式计算

ClickHouse 采用MPP(大规模并行处理)架构,各节点对等协作,通过数据分片与副本机制实现水平扩展。

MPP 与传统计算引擎不同,ClickHouse采用存储计算一体化设计,使存储层能针对查询需求进行深度优化。

2.1 架构核心:存储服务于计算

传统大数据引擎(如 Spark)是 “计算 存储 分离 ”模式——计算层需从外部存储(HDFS/S3)读取原始数据,再进行计算;

而 ClickHouse 是 “计算 存储 一体化 ”模式 —— 自带存储层,可在存储阶段为计算做优化(如列存储、预排序),即“存储服务于计算”,大幅减少计算时的 I/O 开销。

Mermaid

2.2 MPP 架构:并行计算提效

ClickHouse 采用大规模并行处理(MPP) 架构,集群中所有节点对等,无主从之分。

MPP 架构中,查询 任务会被拆分为多个子任务,并行分发到不同节点执行,最后汇总结果,充分利用多核 CPU 与多节点资源。

Mermaid

image-20250830215120702

3、 ClickHouse 核心性能优化特性

ClickHouse 的极速查询能力,源于三大核心优化:

  • 列式存储
  • 向量化执行
  • 数据预排序。

3.1 列式存储:减少无效数据读取

传统行存储按“行”存储数据(一行所有列存在一个文件),查询时需读取整行数据;

而列式存储按“列”拆分存储(每列单独一个文件),查询时仅读取所需列,同时同列数据特征相似,压缩率更高(生产中常达 8:1)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Mermaid

3.2 向量化执行:提升 CPU 效率

传统计算引擎“单条数据处理”(一次处理 1 条数据,CPU 缓存命中率低);

ClickHouse 采用“批量处理”,将数据分成固定大小的块(如 1024 条),借助 CPU 的 SIMD(单指令多数据) 指令集,1 条指令处理多个数据,大幅提升 CPU 利用率。

image-20250830215201216

Mermaid

3.3 数据预排序:加速范围查询

ClickHouse 基于类 LSM 算法,数据写入时先在内存排序,刷盘后通过后台 Compaction 合并为有序大分区,确保磁盘数据始终有序。

查询时可利用有序性快速定位范围(如“时间>2024-01-01”),减少扫描的数据量。

MergeTree (本地表) 建表语句的 示例


-- 示例:创建 MergeTree 表(核心表引擎,支持预排序)
CREATE TABLE IF NOT EXISTS user_behavior (
    user_id UInt64,          -- 用户ID(无符号整数)
    behavior_type String,    -- 行为类型(点击/购买)
    create_time DateTime,    -- 行为时间
    product_id UInt64        -- 商品ID
) ENGINE = MergeTree()      -- MergeTree:支持预排序、Compaction
ORDER BY (user_id, create_time)  -- 排序键:决定数据存储顺序(核心配置)
PARTITION BY toYYYYMM(create_time)  -- 按年月分区(优化时间范围查询)
PRIMARY KEY user_id;        -- 主键:需为排序键前缀(此处user_id是排序键首字段)

-- 关键解释:
-- 1. ORDER BY 是预排序的核心,查询常按 user_id+时间范围过滤,有序数据可快速定位
-- 2. PARTITION BY 按时间分区,查询时先定位分区,再扫描分区内数据

4、 ClickHouse 表引擎

表引擎决定了数据的存储方式、查询支持能力、并发控制等

4.1 ClickHouse 提供多种引擎

ClickHouse 提供多种引擎,适配不同场景,常用引擎如下:

表引擎类型 代表引擎 核心用途 适用场景
核心存储引擎 MergeTree 海量结构化数据存储(支持预排序、分区) 业务核心表(如用户行为、订单表)
分布式引擎 Distributed 管理分片集群(不存储数据,仅路由请求) 分布式查询入口(对外提供统一表名)
内存引擎 Memory 内存存储(数据重启丢失) 临时计算、小表缓存
日志引擎 Log 轻量存储(无索引) 日志数据(如审计日志、访问日志)

Distributed 关键解释:

  • Distributed 表本身不存数据,仅作为“路由入口”
  • 写入时:按分片键计算分片ID,路由到对应节点的本地表
  • 查询时:向所有分片发送子查询,汇总结果后返回

Distributed 表建表示例(分片管理)


-- 前提:已在 config.xml 配置集群 my_cluster(2个分片,每个分片1个副本)
CREATE TABLE IF NOT EXISTS user_behavior_dist (
    user_id UInt64,
    behavior_type String,
    create_time DateTime,
    product_id UInt64
) ENGINE = Distributed(
    my_cluster,          -- 集群名称(配置文件中定义)
    default,             -- 数据库名(与本地表一致)
    user_behavior,       -- 本地表名(每个分片节点上的表)
    user_id              -- 分片键(按user_id哈希分片,保证数据均匀)
);



4.2 Distributed 与 MergeTree 引擎在 ClickHouse 中的协作关系

在 ClickHouse 的高性能列式数据库架构中,Distributed 引擎和 MergeTree 系列引擎有着密切的协同关系,共同构建了 ClickHouse 的分布式处理能力。

Distributed 与 MergeTree 引擎 对比

特性 Distributed 引擎 MergeTree 引擎
本质 逻辑引擎(无实际存储) 物理存储引擎
作用 分布式查询与写入协调 本地数据存储与处理
存储 不存储实际数据 管理本地磁盘数据
数据分布 跨节点/分片 分区内有序存储
依赖关系 依赖底层的 MergeTree 表 作为 Distributed 表基础

Mermaid

4.3 Distributed 与 MergeTree 引擎协作机制说明

Distributed 和 MergeTree 引擎在 ClickHouse 中各司其职:

  • MergeTree 引擎提供高性能本地存储和处理, 作为物理存储基础,每个分片上有独立的MergeTree实例
  • Distributed 引擎实现透明分布式访问层, 提供 分布式访问入口
  • 协作关系:Distributed 表是逻辑入口,MergeTree 表是物理基础

4.3.1. 数据写入流程

Mermaid

4.3.2. 查询处理流程

Mermaid

4.3.3. 实际应用场景


-- 创建本地存储表
CREATE TABLE events_local ON CLUSTER main_cluster
(
    event_date Date,
    event_type String,
    user_id UInt64,
    value Float64
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/events_local', '{replica}')
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, event_type);

-- 创建分布式访问表
CREATE TABLE events_distributed AS events_local
ENGINE = Distributed('main_cluster', 'default', 'events_local', rand());

5、 ClickHouse 数据类型

ClickHouse 支持 100+ 数据类型,设计上兼顾“存储效率”与“计算性能”,核心类型分类及实践如下:

核心数据类型表

类型分类 具体类型 用途说明 示例
基本数值类型 UInt8/UInt64 无符号整数(存储ID、计数,节省空间) user_id UInt64
Int8/Int64 有符号整数(存储带正负的数值) score Int32
Decimal(P,S) 高精度小数(金额、汇率,避免浮点误差) amount Decimal(18,2)
日期时间类型 Date 日期(YYYY-MM-DD,仅占2字节) create_date Date
DateTime64(N) 带精度的时间(N为小数位数,如毫秒) pay_time DateTime64(3)
复杂类型 Array(T) 同类型数组(存储多值,如商品分类) tags Array(String)
Nested(k1 T1, k2 T2) 嵌套结构(类似JSON数组对象) addr Nested(street String, city String)
优化类型 LowCardinality(T) 低基数优化(值数量<1万,如状态) status LowCardinality(String)
Nullable(T) 支持空值(需额外存储空标记,慎用) email Nullable(String)

复杂类型使用示例


-- 示例:含复杂类型的商品表
CREATE TABLE IF NOT EXISTS product_info (
    product_id UInt64,
    name String,
    category Array(String),  -- 数组:商品所属多个分类
    price Decimal(18,2),
    status LowCardinality(String),  -- 低基数:状态仅“在售/下架/预售”
    update_time DateTime64(3),
    remark Nullable(String)  -- 可空:备注可能为空
) ENGINE = MergeTree()
ORDER BY product_id;

-- 插入数据(数组用[],空值用NULL)
INSERT INTO product_info VALUES (
    1001,
    "5G手机",
    ["电子设备", "手机", "5G"],  -- 数组值
    3999.00,
    "在售",  -- 低基数值
    now64(3),  -- 当前毫秒时间
    NULL       -- 空值
);

-- 关键解释:
-- 1. LowCardinality:将“在售”编码为0、“下架”为1,存储时用编码值,减少空间
-- 2. Array:查询时用 HAS 关键字(如 category HAS '5G')快速过滤

6、 分片与副本策略

在 ClickHouse 中,为了提升查询性能及增加数据容错性,分别在水平方向和垂直方向上划分为分片(shard)及副本(replica)。

image-20250830215223742

在分布式模式下,ClickHouse 会将数据在水平方向上分为多个分片(shard),并且分布到不同节点上,不同分片间的数据不同。

分片(shard) 的目的主要是为了提升查询性能,方便多线程及分布式查询。

在分布式查询时,按照分片数量拆分成若干个对本地表的子查询,然后依次查询每个分片的数据,再合并汇总返回。

6.1 分片:水平拆分数据

将数据按“分片键”拆分为多个部分(分片),分布到不同节点,查询时并行处理各分片数据,提升效率。分片键选择需满足“数据均匀分布”(如 user_id、order_id)。

分片策略:

  • 哈希分片:按分片键哈希值分配(如 user_id % 分片数,最常用);
  • 随机分片:按随机数分配(数据均匀,但同一用户数据可能跨分片);
  • 范围分片:按分片键范围分配(如时间范围,适合按时间查询的场景)。

6.2 副本:垂直冗余数据

同一分片的数据在多个节点存储副本,借助 ZooKeeper 实现副本同步(写入一个副本后,自动同步到其他副本),避免单点故障。

副本选择策略(负载均衡)

查询时,ClickHouse 按 load_balancing 参数选择副本,常用策略:

  • Random(默认):优先选择错误数最少的副本,再随机;
  • Round Robin:循环选择副本,避免单点压力;
  • Nearest hostname:选择主机名最相似的副本(减少网络延迟)。

6.3 分片与副本整体流程

Mermaid

7、ClickHouse 索引设计

索引是加速查询的关键,ClickHouse 主要支持稀疏索引(主键索引)跳数索引,避免传统稠密索引的高存储开销。

7.1 稀疏索引(主键索引)

ClickHouse 将数据按“颗粒(默认 8192 行)”划分,仅为每个颗粒的首行存储主键值(存于 primary.idx),并通过标记文件(.mrk)记录颗粒的磁盘物理地址。查询时先通过索引定位颗粒,再读取颗粒数据,大幅减少扫描范围。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

查询流程:

Mermaid

7.2 跳数索引(辅助索引)

当查询条件不命中主键时,跳数索引可进一步跳过无匹配数据的块,常用类型:

  • minmax 索引:存储每个块的字段最值(适合范围查询,如时间范围);
  • 布隆过滤器索引:快速判断块是否包含目标值(适合等值查询,如 category='5G');
  • set 索引:存储块内所有 distinct 值(适合枚举值查询)。

跳数索引创建示例:


-- 示例1:为时间字段创建minmax索引(优化范围查询)
ALTER TABLE user_behavior 
ADD INDEX idx_create_time create_time TYPE minmax GRANULARITY 8192;
-- GRANULARITY 8192:每8192行生成一个索引条目

-- 示例2:为数组字段创建布隆过滤器索引(优化等值查询)
ALTER TABLE product_info 
ADD INDEX idx_category category TYPE bloom_filter(0.01) GRANULARITY 4096;
-- bloom_filter(0.01):误判率1%,平衡空间与精度

-- 关键解释:
-- 1. 跳数索引需手动创建,默认不开启
-- 2. 选择合适的GRANULARITY:值越小,索引越细,但存储开销越大

7.2 比喻说明

想象一下,ClickHouse 的表就是一本超厚的百科全书(比如《中国大百科全书》)。

1.主键索引(Primary Index)是啥?

  • 它就是书最前面的 总目录。这个目录很稀疏,不会告诉你“苹果”在第几页,只会告诉你“A字母开头的条目”从第1页开始,“B字母开头的条目”从第50页开始……
  • 在 ClickHouse 里,这就是稀疏索引。它通过主键帮你快速定位到数据块(Granule),mark文件就像精确的页码,帮你翻到那一章。

2.那“跳数索引”又是干嘛的?

  • 现在,你想在这本百科全书里找所有 “产地是新疆的水果”
  • 用总目录(主键索引)你只能找到“水果”这个大类在哪(比如在“S”字母 section),但你还是得把“水果”这一整章几十页全部读完,才能找出哪些水果的产地是新疆。这很慢。
  • 跳数索引,就像是有人在书页边上贴了各种各样的便利贴,或者给书做了很多专项小目录
  • 比如,有人做了一份 “产地索引” 的小册子,上面写着:•产地:新疆 -> 出现在第15页、第28页、第33页产地:海南 -> 出现在第22页、第40页
  • 有了这个“产地索引”(也就是跳数索引),你就不用去读“水果”章节的所有几十页了。你直接翻到第15、28、33页看一眼,就能找到所有新疆水果。你跳过了大量根本不相关的书页!

8、ClickHouse 计算引擎

ClickHouse 计算引擎的核心是“将 SQL 转化为可执行计划, 然后 并行执行”

从功能和整体架构上讲,ClickHouse 的计算引擎与 其他数据库(如mysql )的计算引擎并没有很大的不同。

功能都是将可描述的结构化查询语言(SQL)转化翻译为可以执行的物理计划以及执行计算并获得计算结果,而整体架构都有包含 SQL 解析、翻译解释、计算执行等部分。

image-20250830215240797

计算引擎核心流程

Mermaid

从实现方面,ClickHouse 同样采用了多线程及分布式查询,成为其高性能和高扩展性的关键。

通过线程级并行的方式提升性能,利用多核 CPU 的计算能力;

通过采用分布式架构,支持分片和副本,通过将数据分布到多个节点实现存储和计算的扩展。

二、ClickHouse聚合算子实现原理

在ClickHouse中,聚合是最核心的算子之一,也是优化最多的部分。

聚合 底层实现融合了算法设计、数据结构优化和硬件特性利用,最终实现了对海量数据的高效聚合。

在 ClickHouse 中,聚合操作所使用的 Hashmap 中的 Key,本质上, 就是由 GROUP BY 子句计算出的值(分组键)

ClickHouse的聚合实现是“算法设计+硬件特性+场景适配”的结合体,每个优化都针对具体痛点。

理解这些细节,不仅能更好地使用ClickHouse,也能为其他大数据引擎的优化提供思路。

2.1、聚合算法核心架构:从两阶段到并行化

ClickHouse的聚合逻辑核心围绕三个要素展开:HashMap(存储中间结果)、并行计算(提升效率)、两阶段聚合(减少数据传输)。

理解这三个要素,是掌握聚合实现的基础。

2.1.1 聚合流程 的 两个 基础 阶段

常规聚合过程分为pre-aggregate(初步聚合)final-aggregate(最终聚合) 两个阶段:

  • pre-aggregate:多个线程(T1、T2、T3)各自处理部分数据,用HashMap存储本地中间结果(如每个线程计算自己负责的数据的sum/count);
  • final-aggregate:单线程合并所有线程的中间结果,得到最终聚合结果。

image-20250830215259767

存在的问题

如果pre-aggregate后数据量几乎没减少(比如聚合key基数极高),final-aggregate会因单线程处理大量数据而成为瓶颈,严重影响效率。

2.1.2 并行优化:Two-level HashMap的引入

为解决final-aggregate单线程瓶颈,ClickHouse引入Two-level HashMap(二级哈希表),实现final-aggregate的并行化。

2.1.2.1 Two-level HashMap结构

与常规HashMap不同,Two-level HashMap分为两层:

  • 第一层:固定数量的Bucket(默认256个),每个Bucket是一个独立的子HashMap;
  • 第二层:子HashMap内部用开放地址法处理冲突。

数据先通过哈希映射到第一层Bucket,再进入子HashMap处理,天然支持按Bucket拆分并行处理。

Mermaid

image-20250830215401919

2.1.2.2 并行化的final-aggregate流程

引入Two-level HashMap后,final-aggregate可按第一层Bucket拆分任务,

每一个线程可以独立的 处理一个Bucket的子HashMap,避免并发冲突,实现并行合并:

image-20250830215451472

Mermaid

2.1.3 聚合算法的自适应选择

ClickHouse提供了一种运行时自适应的方式,在运行时选择选择聚合算法以及是否使用Two-level HashMap。

具体的策略是当pre-aggregate过程中,HashMap中的数据超过一定阈值,将其转换成Two-level HashMap。

参考参数:

  • group_by_two_level_threshold:HashMap中条目数超过该阈值时,转换为Two-level HashMap;
  • group_by_two_level_threshold_bytes:HashMap占用内存超过该阈值时,转换为Two-level HashMap。

每个pre-aggregate线程独立选择是否是用Two-level HashMap的,那么在进行第二次聚合的时候输入可能既有常规HashMap也有Two-level Hashmap,对于这种情况会在final-aggregate前先将常规HashMap转换成Two-level的。整体的算法如下图所示:

转换逻辑如下:

Mermaid

image-20250830215518905

潜在优化点:目前HashMap到Two-level的转换是单线程执行,若改为并行转换可进一步提升效率。

2.2、HashMap的构建与聚合执行

ClickHouse的计算引擎基于列式存储,数据以Block(由多列组成的二维表)为单位流转。

聚合过程本质是将Block数据写入HashMap并执行聚合计算,分为“构建HashMap”和“执行聚合”两步。

image-20250830215531954

2.2.1 构建HashMap:初始化聚合状态

构建阶段 de 目标 :为所有聚合key分配内存,并初始化聚合函数的状态对象(如sum的初始值0、uniqExact的空HashSet)。

关键流程:

(1) 遍历Block中的每行数据,判断聚合key是否已在HashMap中;

(2) 若不存在,通过 Arena 分配聚合函数状态所需内存, Arena 就是内存池 ;

(3) 初始化状态对象(如sum函数初始化为0);

(4) 记录每行对应的状态对象内存地址,便于后续快速访问。

image-20250830215554550

关键代码解析(内存分配与状态初始化):


/// 1. 为所有聚合函数状态分配内存
/// total_size_of_aggregate_states:所有聚合函数状态的总内存大小
/// aggregates_pool:Arena内存池(统一管理内存,便于监控和释放)
/// place:分配到的连续内存块起始地址
AggregateDataPtr place = result.aggregates_pool->alignedAlloc(
    total_size_of_aggregate_states,  // 总大小
    align_aggregate_states            // 内存对齐要求
);

/// 2. 初始化每个聚合函数的状态对象
/// offsets_of_aggregate_states:每个聚合函数在总内存中的偏移量
for (size_t j = 0; j < aggregate_functions.size(); ++j)
{
    // 为第j个聚合函数创建状态对象(如sum创建AggregateFunctionSumData<Int64>)
    aggregate_functions[j]->create(place + offsets_of_aggregate_states[j]);
}

为什么用Arena而不是new?

聚合可能消耗大量内存,Arena可集中管理内存,便于监控总占用量、限制单查询内存使用,或在内存不足时将状态 spill 到磁盘。

Arena 是什么?

Netty 源码中也用到了 Arena ,具体请参考 尼恩的 Netty对象池、内存池视频。

简单说,Arena 是一个预分配的大块连续内存(内存池)。当聚合需要为新的分组键分配内存(存储键值或聚合状态)时,不再频繁调用系统的 newmalloc,而是从 Arena 中“划出”一小段连续空间使用。

2.2.2 执行聚合:更新状态对象

构建完HashMap后,需根据每行数据更新对应的聚合函数状态(如sum累加、count递增)。

由于状态对象分散在不同内存地址,无法通过SIMD指令批量处理,但可针对特殊场景优化。

image-20250831091627656

常规聚合执行代码:


/// 为一个聚合函数批量处理多行数据
void IAggregateFunctionHelper::addBatch(
    size_t row_begin,          // 起始行
    size_t row_end,            // 结束行
    AggregateDataPtr * places, // 每行对应的状态对象地址
    size_t place_offset,       // 状态对象在内存块中的偏移
    const IColumn ** columns,  // 输入列数据
    Arena * arena,             // 内存池(可能用于动态分配,如HashSet扩容)
    ssize_t if_argument_pos    // 条件列位置(可选)
) const override
{
    // 遍历行,更新对应状态
    for (size_t i = row_begin; i < row_end; ++i)
        if (places[i])  // 若状态对象存在(已初始化)
            // 调用具体聚合函数的add方法(如sum累加)
            static_cast<const Derived *>(this)->add(
                places[i] + place_offset,  // 状态对象地址
                columns,                    // 输入数据列
                i,                          // 当前行号
                arena
            );
}

特殊优化:常量key的SIMD加速

若所有聚合key相同(结果只有一条),可通过SIMD指令批量处理:


/// Sum函数针对常量key的批量处理(用SIMD加速)
template <typename Value>
void NO_INLINE AggregateFunctionSumData::addMany(
    const Value * __restrict ptr,  // 输入数据指针(__restrict避免指针别名,优化编译)
    size_t start, 
    size_t end
)
{
#if USE_MULTITARGET_CODE
    // 根据CPU支持的指令集选择最优实现
    if (isArchSupported(TargetArch::AVX512BW))
    {
        addManyImplAVX512BW(ptr, start, end);  // 用AVX512BW指令
        return;
    }
    else if (isArchSupported(TargetArch::AVX2))
    {
        addManyImplAVX2(ptr, start, end);      // 用AVX2指令
        return;
    }
    // ... 其他指令集(SSE42等)
#endif
    // 无SIMD支持时的普通实现
    addManyImpl(ptr, start, end);
}

JIT优化:新版本默认开启compile_aggregate_expressions参数,通过即时编译(JIT)生成聚合代码,减少函数调用开销,进一步提升效率。

三、HashMap类型选择策略

HashMap是聚合的核心数据结构,ClickHouse针对不同场景设计了30+种HashMap及哈希函数组合,核心思路是“特例化优化”——根据聚合key的类型和数量选择最优实现。

Mermaid

智能数据结构选择(临界点优化)

分组键特征 选用数据结构 性能临界点
UInt8 小范围(<256) FixedHashMap(数组) 无哈希计算 O(1)
普通整型/字符串 HashMapWithSavedHash 基数 < 1000万
海量基数(>1000万) TwoLevelHashMap 桶分治 + 并行
超长字符串 StringHashMap 短字符串内联存储

3.1 基础结构:开放地址法HashMap

ClickHouse采用开放地址法实现HashMap(而非Java标准库的拉链法),原因如下:

  • 缓存友好:数据存储连续,CPU缓存命中率更高;
  • 无内存碎片:插入时无需动态分配链表节点,减少内存碎片;
  • 实现简单:避免链表遍历,合并HashMap时效率更高。

Mermaid

image-20250830215651687

3.2 按场景优化的HashMap类型

3.2.1 FixedHashMap:适用于低基数整数key

当聚合key是UInt8(0-255)或UInt16(0-65535)时,直接用key值作为索引,无需哈希计算:

  • 内存结构:固定大小的数组(如UInt8对应256个槽位);
  • 优势:无哈希冲突、无需resize、查询O(1)。

image-20250830215705831

Mermaid

3.2.2 StringHashMap:适用于字符串key

字符串key长度可变,StringHashMap内置多个子HashMap,按字符串长度路由:

  • 短字符串(≤8字节):用UInt64存储,哈希函数基于SSE42的_mm_crc32_u64(硬件加速);
  • 中长字符串(≤24字节):拆分后用多个UInt64处理;
  • 长字符串:用常规哈希函数。

Mermaid

image-20250830215734317

哈希函数示例(SSE42加速)


struct StringHashTableHash
{
#if defined(__SSE4_2__)
    // 处理8字节字符串key(StringKey8本质是UInt64)
    size_t ALWAYS_INLINE operator()(StringKey8 key) const
    {
        size_t res = -1ULL;  // 初始值
        res = _mm_crc32_u64(res, key);  // 用硬件CRC32指令计算哈希
        return res;
    }
    
    // 处理16字节字符串key(拆分为两个UInt64)
    size_t ALWAYS_INLINE operator()(StringKey16 key) const
    {
        size_t res = -1ULL;
        res = _mm_crc32_u64(res, key.items[0]);  // 第一部分
        res = _mm_crc32_u64(res, key.items[1]);  // 第二部分
        return res;
    }
    // ... 其他长度处理
#endif
};

3.2.3 TwoLevelHashMap:支持并行聚合

如前文所述,TwoLevelHashMap含256个固定子HashMap,通过哈希值前8位选择子HashMap,剩余位用于子HashMap内部定位。

其变种TwoLevelHashMapWithSavedHash会缓存哈希值,合并时无需重复计算,进一步提速。

image-20250830215748184

四、ClickHouse Group By 执行流程

由于平台篇幅限制, 剩下的内容(5000字+),请参参见原文地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

posted @ 2025-09-05 16:41  技术自由圈  阅读(17)  评论(0)    收藏  举报