数据库浅谈之 DuckDB AGG 底层实现

news2025/7/29 20:29:56

数据库浅谈之 DuckDB AGG 底层实现

HELLO,各位博友好,我是阿呆 🙈🙈🙈

这里是数据库浅谈系列,收录在专栏 DATABASE 中 😜😜😜

本系列阿呆将记录一些数据库领域相关的知识 🏃🏃🏃

OK,兄弟们,废话不多直接开冲 🌞🌞🌞


一 🏠 概述

DuckDB 关于 AGG 的算子有三个

PhysicalUngroupedAggregate,适用于只聚合无分组,无DISTINCT,且所有行都可被合并

PhysicalPerfectHashAggregate,适用于执行一组分组和聚合,使用一个完美哈希表

PhysicalHashAggregate 是实现分组和聚合的物理算子,哈希表执行分组


本篇是对多数 AGG 场景普遍适用的 PhysicalHashAggregate 源码剖析


1.1 核心类图展示

核心类图一

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pgZIzekK-1677203088915)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\类图描述1-1660029527714-1677141219014.png)]

核心类图二

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZbkuhdzT-1677203088917)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\类图描述2_copy-1677141219014.png)]


1.2 核心类成员描述

基类 PhysicalOperator

它是执行计划中物理算子的基类

类成员作用
PhysicalOperatorType type算子类型
vector<unique_ptr<PhysicalOperator>> children子算子集
vector<LogicalType> types算子返回类型
idx_t estimated_cardinality算子预估值
unique_ptr<GlobalSinkState> sink_state全局接收状态
unique_ptr<GlobalOperatorState> op_state算子全局状态
类函数作用
virtual SinkResultType Sink(ExecutionContext &context, GlobalSinkState &gstate, LocalSinkState &lstate,DataChunk &input) const;重复调用至无输入,可并行调用,访问 GlobalSinkState 需加锁
virtual void Combine(ExecutionContext &context, GlobalSinkState &gstate, LocalSinkState &lstate) const;单线程完成其 pipeline 模块部分时调用,可并行调用,最后一次访问 LocalSinkState
virtual SinkFinalizeType Finalize(Pipeline &pipeline, Event &event, ClientContext &context,GlobalSinkState &gstate) const;当所有线程执行完成时调用,单线程调用,每个pipeline 只调用一次
virtual void GetData(ExecutionContext &context, DataChunk &chunk, GlobalSourceState &gstate, LocalSourceState &lstate) const;Source interface,向上层 pipeline 发送数据
static idx_t GetMaxThreadMemory(ClientContext &context);算子每个线程可使用的最大内存
void AddPipeline(Executor &executor, shared_ptr<Pipeline> current, PipelineBuildState &state);添加 Pipeline
virtual void BuildPipelines(Executor &executor, Pipeline &current, PipelineBuildState &state);创建 Pipeline
void BuildChildPipeline(Executor &executor, Pipeline &current, PipelineBuildState &state,PhysicalOperator *pipeline_child);创建子 Pipeline

PhysicalHashAggregate

它是实现分组和聚合的物理算子,哈希表执行分组

类成员作用
vector<unique_ptr<Expression>> groupsgroup by 分组项
vector<GroupingSet> grouping_setsgrouping set 分组项
vector<unique_ptr<Expression>> aggregatesAgg 函数项
bool any_distinct聚合函数中是否有 DISTINCT
vector<LogicalType> group_types分组项的各个类型
vector<LogicalType> payload_types聚合函数参数列表的类型
vector<LogicalType> aggregate_return_types聚合返回的类型
vector<RadixPartitionedHashTable> radix_tables基数分区哈希表 (一个分组一个)
vector<BoundAggregateExpression *> bindings指向聚合函数的指针集
// 分组函数, 类似于 grouping_set 这种
vector<vector<idx_t>> grouping_functions

// 聚合函数存在过滤,记录对应的过滤 Expression 和索引
unordered_map<Expression *, size_t> filter_indexes
类函数
void GetData(ExecutionContext &context, DataChunk &chunk, GlobalSourceState &gstate,LocalSourceState &lstate) const override;
SinkResultType Sink(ExecutionContext &context, GlobalSinkState &state, LocalSinkState &lstate,DataChunk &input) const override;
void Combine(ExecutionContext &context, GlobalSinkState &state, LocalSinkState &lstate) const override;
SinkFinalizeType Finalize(Pipeline &pipeline, Event &event, ClientContext &context,GlobalSinkState &gstate) const override;
//不切换此选项,GetData 方法将在扫描哈希表时销毁该哈希表
static void SetMultiScan(GlobalSinkState &state);

PhysicalHashAggregate 的 Sink 过程

HashAggregateGlobalState

类成员作用
vector<unique_ptr<GlobalSinkState>> radix_states记录(监控)基数分区哈希表的GlobalSinkState 状态

HashAggregateLocalState

类成员作用
DataChunk aggregate_input_chunk聚合输入数据块
vector<unique_ptr<LocalSinkState>> radix_states记录(监控)基数分区哈希表的LocalSinkState 状态

PhysicalHashAggregate 的 Source 过程

PhysicalHashAggregateGlobalSourceState

类成员作用
vector<unique_ptr<GlobalSourceState>> radix_states记录(监控)基数分区哈希表的GlobalSinkState 状态

PhysicalHashAggregateLocalSourceState

类成员作用
vector<unique_ptr<LocalSourceState>> radix_states记录(监控)基数分区哈希表的LocalSinkState 状态

RadixPartitionedHashTable

类成员作用
GroupingSet &grouping_setgroup by 分组项
vector<idx_t> null_groups把不在grouping_set中的分组项放到这
const PhysicalHashAggregate &opHashAgg 算子
idx_t radix_limit切换到基数分区前,算子中可有多少分组
vector<Value> grouping_values属于此哈希表的分组值
类函数作用
void Sink(ExecutionContext &context, GlobalSinkState &state, LocalSinkState &lstate, DataChunk &input,DataChunk &aggregate_input_chunk) const;将输入数据拆分为分组和聚合两部分;通过逻辑判断,创建不同的哈希表实例;调用哈希表实例的 AddChunk 方法
void Combine(ExecutionContext &context, GlobalSinkState &state, LocalSinkState &lstate) const;把各个哈希表的哈希桶释放掉,然后把RadixHTLocalState 的成员PartitionableHashTable 推到全局
bool Finalize(ClientContext &context, GlobalSinkState &gstate_p) const;未分区时就是把分区哈希表的数据和全局哈希表进行依次合并,释放分区哈希表
void ScheduleTasks(Executor &executor, const shared_ptr<Event> &event, GlobalSinkState &state,vector<unique_ptr<Task>> &tasks) const;未分区时,不会有并行任务队列;分区时可并行计算
void GetData(ExecutionContext &context, DataChunk &chunk, GlobalSinkState &sink_state, GlobalSourceState &gstate_p,LocalSourceState &lstate_p) const;向上输出数据的接口,调用 GroupedAggregateHashTableScan 接口
unique_ptr<GlobalSourceState> GetGlobalSourceState(ClientContext &context) const;因此在这里会进行聚合函数的计算?构造并获取 GlobalSourceState
unique_ptr<GlobalSinkState> GetGlobalSinkState(ClientContext &context) const;还是说在 sink 已经完成了计算?构造并获取 GlobalSinkState
unique_ptr<LocalSinkState> GetLocalSinkState(ExecutionContext &context) const;构造并获取 LocalSinkState
unique_ptr<LocalSourceState> GetLocalSourceState(ExecutionContext &context) const;构造并获取 LocalSourceState

RadixPartitionedHashTable 的 source 过程

RadixHTGlobalSourceState

类成员作用
mutex lock互斥锁,用于成员共享变量
idx_t ht_index哈希表索引
idx_t ht_scan_position哈希表扫描位置,相当于数据拷贝时的列偏移
atomic<bool> finished原子变量,是否数据拷贝完成

RadixHTLocalSourceState

类成员作用
DataChunk scan_chunk向上输出的数据

RadixPartitionedHashTable 的 sink 过程

RadixHTGlobalState

类成员作用
vector<unique_ptr<PartitionableHashTable>> intermediate_htsCombine 阶段,接受推向全局的分区哈希表
vector<unique_ptr<GroupedAggregateHashTable>> finalized_hts最终的结果哈希表
bool is_empty哈希表是否为空
bool multi_scan哈希表是否支持多次扫描
mutex lock用于更新全局聚合状态锁
atomic<idx_t> total_groups聚合数
bool is_finalized = false是否结束
bool is_partitioned = false是否分区
RadixPartitionInfo partition_info分区数,分区掩码和前缀等

RadixHTLocalState

类成员作用
DataChunk group_chunk分组数据
unique_ptr<PartitionableHashTable> ht用于单个分组的哈希表
bool is_empty哈希表是否有数据

PartitionableHashTable

类成员作用
Allocator &allocatorSTL 类似,申请空间但并不实例化
BufferManager &buffer_managerBlock 管理类
vector<LogicalType> group_types分组类型
vector<LogicalType> payload_types聚合函数返回的类型
vector<BoundAggregateExpression *> bindings聚合函数
bool is_partitioned是否分区
RadixPartitionInfo &partition_info当做分区时确定桶落在X分区
HashTableList unpartitioned_hts未分区哈希表集合
unordered_map<hash_t, HashTableList> radix_partitioned_hts分区哈希表集合
类函数作用
idx_t AddChunk(DataChunk &groups, DataChunk &payload, bool do_partition)为哈希表插入数据
void Partition()将哈希表分区
bool IsPartitioned()判断是否需要分区
HashTableList GetPartition(idx_t partition)获取分区哈希表
HashTableList GetUnpartitioned()获取未分区哈希表
void Finalize()遍历将哈希表的哈希桶销毁

GroupedAggregateHashTable

类成员作用
HtEntryType entry_type哈希值类型是 32/64 bit
idx_t entries哈希桶数(元素数)
vector<BufferHandle> payload_hds哈希表数据块
vector<data_ptr_t> payload_hds_ptrs上个成员的 data 指针
BufferHandle hashes_hdl哈希桶数据块
data_ptr_t hashes_hdl_ptr上个成员的 data 指针
idx_t hash_offset内存布局中哈希值的偏移量
hash_t hash_prefix_shift取高十六位前缀
hash_t bitmask哈希掩码
类函数作用
idx_t AddChunk(DataChunk &groups, DataChunk &payload)列式计算哈希值
idx_t FindOrCreateGroups(DataChunk &groups, Vector &group_hashes, Vector &addresses_out, SelectionVector &new_groups_out)创建哈希桶和插入分组数据,初始化聚合函数
idx_t AddChunk(DataChunk &groups, Vector &group_hashes, DataChunk &payload)更新聚合函数项
void Combine(GroupedAggregateHashTable &other)合并哈希表
idx_t Scan(idx_t &scan_position, DataChunk &result)进行数据拷贝,向上输出结果 DataChunk

BaseAggregateHashTable

类成员作用
Allocator &allocatorSTL 类似,申请空间但并不实例化
BufferManager &buffer_managerBlock 管理类
RowLayout layout行内存布局

RowLayout

类成员作用
Aggregates aggregates聚合函数
idx_t flag_width头部校验位的宽度
idx_t data_width数据宽度
idx_t aggr_width聚合函数宽度
idx_t row_width行宽
vector<idx_t> offsets偏移量
bool all_constant是否存在变长数据
idx_t heap_pointer_offset堆指针偏移

1.3 哈希表

哈希表结构

GroupedAggregateHashTable 是 PhysicalHashAggregate 用于计算的线性探测哈希表,使用开放寻址解决哈希冲突。输入分组和聚合函数,将计算结果存储在哈希表中,它由两部分组成,分别是 hashes(哈希部分) 和 payload(聚合部分)


HASHES LAYOUT

[SALT][PAGE_NR][PAGE_OFFSET]

内存布局作用
SALT哈希值高位,例对于 64 位哈希,为 16
PAGE_NR缓冲区管理的 payload 页面索引
PAGE_OFFSET指向 payload page 的逻辑条目偏移量

PAYLOAD LAYOUT

[VALIDITY][GROUPS][HASH][PADDING][PAYLOAD]

内存布局作用
VALIDITY数据列的有效位
GROUPS分组数据,大小固定,可为多列
HASH分组的哈希数据
PADDING对齐数据
PAYLOAD聚合数据

哈希表结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CDYdTpPo-1677203088920)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660116154837-1677141219014.png)]


桶和数据拆开原因

时间角度:哈希桶和数据块绑定导致无法使用列连续特性(指针 + 偏移量),快速定位到某行的哈希桶

空间角度:在所有哈希表 Sink 阶段完成时(所有输入行计算完毕时),在 combine 阶段即可销毁哈希桶,释放掉 Block 块内存


哈希表实现原理

在哈希表创建时,申请一个 block 块(一段连续内存)作为哈希桶,在计算哈希值时通过输入分组数据,列计算和与操作计算出每行哈希值,哈希构建过程如下图所示


DuckDB 列计算哈希

如下图所示,输入数据可理解为 MDP (列存),哈希值批计算,充分利用列连续特性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4oFKSeO7-1677203088920)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660041232806-1677141219014.png)]


DuckDB 构建哈希表

1、列算哈希

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qoxHiaEl-1677203088921)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660117247240-1677141219014.png)]

2、开放寻址

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kuBvFrQy-1677203088922)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660117355809-1677141219014.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l53HqJa4-1677203088923)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660117395634-1677141219014.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GCor9Zx2-1677203088924)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660117417032-1677141219014.png)]

3、数据拷贝

遍历桶个数,拷贝分组输入 Trunk 填充数据块

所有数据拷贝操作,均调用 C 函数库,效率高

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1F9nBNkS-1677203088924)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660119186566-1677141219014.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TZ7sBRZw-1677203088925)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660119214543-1677141219014.png)]

4、初化聚合

初始化 PayLoad 部分,遍历桶和聚合函数项,按对应的聚合函数类型去构筑空间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CtaYokLp-1677203088925)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660122505761-1677141219015.png)]

struct AvgState { uint64_t count; double value; //func... };

5、数据比对

遍历命中哈希表的行,数据比较(指针+偏移,类型强转)

如果数据不一致(即两行数据不完全相同,不落在一个分组),标记该行;比较结束后,被标记行再次重新往后寻找哈希桶,直到所有的行都被聚合

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hcb2XYHN-1677203088926)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660122617452-1677141219015.png)]


6、更新聚合

遍历行,更新 PayLoad 部分,各函数哈希函数内的 count 和 value 值


二 🏠 核心

2.1 HT 优化策略

HT 选取

哈希冲突的解决 链接 或 线性探测 ,使用链接,不直接将聚合值保留在哈希表中,而是保留一组值和聚合列表。如果分组值指向具有空列表的哈希表条目,则只需添加新组和聚合。如果分组值指向现有列表,检查每个列表条目分组值是否匹配。匹配则更新该组聚合。不匹配,我们创建一个新的列表条目。

在线性探测中没有这样的列表,在找到现有条目时,将比较分组值,匹配则将更新条目。不匹配则在哈希表中向下移动一个条目并重试。当找到匹配的组条目或找到空的哈希表条目时,此过程完成。

虽然理论上等效,由于缓存局部性,计算机硬件架构将倾向于线性探测。因为线性探测遍历哈希表条目线性地,下一个条目很可能在 CPU 缓存中,因此访问速度更快。在现代硬件架构上,链接通常会导致随机访问和更差的性能。因此,我们对聚合哈希表采用了线性探测


HT优化

当冲突太多,即太多的组散列到同一个散列表条目,链接和线性探测都会在理论上从 O(1) 到 O(n) 降低散列表大小的查找性能。这个问题的一个常见解决方案是在 填充率 超过某个阈值时调整哈希表大小,例如 75% 是 Java 的默认值HashMap. 这一点特别重要,因为在开始聚合之前我们不知道结果中的组数量。我们也不假设知道输入表中的行数

因此,我们从一个相当小的哈希表开始,并在填充率超过阈值时调整它的大小。基本的哈希表结构如下图所示,该表有四个槽 0-4。表中已经存在三个组,组键为 12、5 和 2。每个组的聚合值(例如来自 a SUM)为 43 等

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kWQcWMDg-1677203088926)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1663555013913.png)]

在调整大小后调整部分填充的哈希表的大小是一个很大的挑战,所有组都在错误的位置,我们必须移动所有内容,这将非常昂贵

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pv800l0y-1677203088927)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1663555070058.png)]

为了有效地支持调整大小,我们实现了一个由单独分配的指针数组组成的两部分聚合哈希表,该指针数组指向包含分组值和每个组的聚合状态的有效负载块。指针不是实际指针而是符号指针,它们指的是块 ID 和所述块内的行偏移量。如上图所示,哈希表条目分为两个有效负载块。在调整大小时,我们丢弃指针数组并分配一个更大的数组。然后,我们再次读取所有有效负载块,对组值进行哈希处理,并将指向它们的指针重新插入到新的指针数组中。组数据因此保持不变,这大大降低了调整哈希表大小的成本

这可以在下图中看到,我们将指针数组大小加倍,但有效负载块保持不变

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-30saUeOF-1677203088928)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1663555189188.png)]


简单的两部分哈希表设计需要在调整大小时重新哈希 所有 组值,这可能非常昂贵,尤其是对于字符串值。为了加快速度,我们还将组值的原始哈希写入每个组的有效负载块。然后,在调整大小期间,我们不必重新散列组,而是可以从有效负载块中读取它们,计算新的偏移量到指针数组中,然后插入那里

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3vLo8cVa-1677203088928)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1663555366962.png)]


两部分哈希表在查找条目时有一个很大的缺点:指针数组和有效负载块中的组条目之间没有排序。因此,跟随指针会在内存层次结构中创建随机访问。这将导致计算中不必要的停顿。为了缓解这个问题,我们扩展了指针数组的内存布局,除了指向有效负载值的指针之外,还包括来自组哈希的一些(1 或 2)字节。这样,线性探测可以首先将指针数组中的哈希位与当前组哈希进行比较,并决定是否值得跟踪有效负载指针。对于指针链中的每个组,这可能会继续。只有当哈希位匹配时,我们才必须实际跟随指针并比较实际的组这种优化大大减少了必须遵循指向有效负载块的指针的次数,从而减少了与整体性能直接相关的随机访问内存的次数。它还具有很好的副作用,即也大大减少了也可能很昂贵的完整组比较,例如在包含字符串的组上进行聚合时

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k8q4bu4j-1677203088929)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1663555484211.png)]

这里的另一个(较小的)优化涉及指针数组条目的宽度。对于具有很少条目的小型哈希表,我们不需要很多位来编码有效负载块偏移指针。DuckDB 支持 4 字节和 8 字节指针数组条目。

对于大多数聚合查询,绝大多数查询处理时间都花在查找哈希表条目上,这就是为什么值得花时间优化它们的原因。如果你很好奇,所有这些的代码都在 DuckDB 存储库中,aggregate_hashtable.cpp. 当我们知道列统计信息中只有几个不同的组时,还有另一个优化,完美的哈希聚合,但那是另一篇文章。但我们还没有在这里完成。


2.2 并行分组聚合

相关论文

随着现代计算机体系结构的发展,两个问题共同阻碍了最先进的并行查询执行方法:

(i) 为了利用多核的优势,所有的查询工作必须均匀地分布在(很快)数百个线程中,以实现良好的加速

(ii) 由于现代乱序核的复杂性,即使使用精确的数据统计也很难平均分配工作

因此,现有的 plandriven 并行方法会遇到负载平衡和上下文切换瓶颈,因此不再具有可伸缩性。

多核体系结构面临的第三个问题是 内存控制器的去中心化,这将导致非统一内存访问(NUMA)


作为回应,我们提出了 “片段驱动的” 查询执行框架,其中调度变成了支持 NUMA 的细粒度运行时任务。morsel 驱动的查询处理获取输入数据的小片段(“morsels”),并将它们调度到运行整个操作符 pipeline 的工作线程,直到下一个管道中断。并行度没有嵌入到计划中,但是可以在查询执行期间弹性地改变,因此调度程序可以对不同片段的执行速度做出反应,也可以动态地调整资源,以响应工作负载中新到达的查询。此外,调度程序知道 NUMA 本地片段和操作符状态的数据局部性,因此绝大多数执行都发生在 NUMA 本地内存上。我们对TPC-H和SSB基准测试的评估显示,在32核的情况下,它的绝对性能非常高,平均加速超过30


并行分组聚合

每个线程从下层算子读取数据并构建单独的本地哈希表,然后从单个线程将它们合并在一起。在分组列很少时,这将非常有效。如果组很少,单个线程可以合并许多线程本地哈希表,而不会产生瓶颈。但是完全有可能有与输入行一样多的组,例在可能成为主键候选的列上进行分组时(KEY 值极高场景),DuckDB 解决方式是采用 相关论文 的 4.4 Grouping/Aggregation 中提到的并行聚合方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LDzxShTi-1677203088930)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1663059363334.png)]


若两个组具有不同的哈希值,它们不可能相同。因此所有线程使用相同的分区方式,就可以使用哈希值创建组的完全独立分区,而无需线程之间的任何通信(参见上图中的阶段 1)【即各线程的分区逻辑(计算哈希值)一致


在构建完所有本地哈希表之后,为每个工作线程分配单独的分区,并将该分区内的哈希表合并在一起(参见上图中的阶段 2)。因为分区是在散列上使用基数分区方案创建的,所以所有工作线程都可以独立地合并各自分区内的散列表。结果是正确的,因为每个组都进入一个分区并且仅该分区 【即将各个线程的 HT 条目所对应的分区聚合


不需要构建一个包含所有组的最终(可能是巨大的)哈希表,因为基组分区确保每个组都本地化到一个分区 【即各分区数据可以直接输出


2.3 计算流程

逻辑走向

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PfFcvCQp-1677203088930)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\时序图描述1-1677141219015.png)]

构建哈希表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w3kyaaf9-1677203088930)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\时序图描述2-1677141219015.png)]


Combine

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gOw5kmKk-1677203088931)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\时序图描述3-1660211041028-1677141219015.png)]


Finalize

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kLNCo2ID-1677203088932)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\时序图描述4-1660211220746-1677141219015.png)]


GetData

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mfy3fPbc-1677203088933)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\时序图描述5-1677141219015.png)]


分区逻辑

未分区哈希表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vwP8ok3P-1677203088933)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660213235214-1677141219015.png)]

当哈希桶达到上限,当前值 *2 的方式进行分区

计算桶落在哪个分区,FlushMoveState(allocator, layout) 拷贝数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FvuUevhY-1677203088934)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660213885894-1677141219015.png)]

将桶数据插入新的哈希表,参考哈希表实现流程

最后调用 RowOperations::CombineStates 将原哈希表的 PayLoad 的数值赋值给 分区的哈希表


2.4 核心优点

计算哈希值

DuckDB 算子间用 DataChunk 传递数据,在 HashAgg 计算哈希值时,采用列式计算,后续列和前列哈希值按行与操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ZuT7yKL-1677203088934)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660215006544-1677141219015.png)]


哈希值使用高十六位的比较方式(哈希值撒盐+右移)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9kl4XRXB-1677203088935)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660215150147-1677141219015.png)]


数据比对

不用构造迭代器,直接指针偏移 + 格式转换

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kbDURbVB-1677203088936)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660215580026-1677141219015.png)]


数据拷贝

数据拷贝过程,不用迭代器,C 风格 memcopy ,效率很高

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h3jLVwXM-1677203088936)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660215710664-1677141219015.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KMpItjpu-1677203088937)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660215743049-1677141219015.png)]


哈希表结构

开放寻址法,桶和数据分开构筑,参考 4.1 提到的时间和空间效率

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SYgQKom7-1677203088938)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660215846649-1677141219018.png)]


聚合函数

所有的聚合函数均使用函数指针以执行绑定操作,HashAgg 算子本身不关注聚合类型。在向上传输数据时,分组列和聚合函数值,本身就在同一块内存区域,利于数据拷贝

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T8bjDXDW-1677203088939)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660268527449-1677141219018.png)]


由聚合函数本身提供相关的具体实现,以 AVG 举例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-njbc6gxu-1677203088939)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660268619860-1677141219018.png)]


PlayLoad 初始化和合并

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g2HTEAbn-1677203088939)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660268707216-1677141219018.png)]


在分组结束后,按行依次更新聚合函数值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CwDfC98a-1677203088940)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660268845729-1677141219018.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5gblWVkN-1677203088940)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660268909360-1677141219018.png)]


在 GetData 时调用 Finalize ,完成最后的聚合计算

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ecs0SJwc-1677203088940)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660268656097-1677141219018.png)]


哈希表分区

在当前哈希表桶数超过默认最大限制(1024)且支持分区时,将当前哈希表进行拆分成多个哈希表,由 RadixPartitionedHashTable 负责开线程(走 PipelineTask 的资源管控)

这种哈希表的分区方式,使每一个哈希表桶基数不大,减轻行数据的寻址成本

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A7rca6Uh-1677203088941)(E:\2022年MD文档\2023 年 MD文档\二月\数据库浅谈\数据库浅谈之 DuckDB AGG 底层实现.assets\1660269735245-1677141219018.png)]

三 🏠 结语

身处于这个浮躁的社会,却有耐心看到这里,你一定是个很厉害的人吧 👍👍👍

各位博友觉得文章有帮助的话,别忘了点赞 + 关注哦,你们的鼓励就是我最大的动力

博主还会不断更新更优质的内容,加油吧!技术人! 💪💪💪

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

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

相关文章

离线维基百科阅读器Kiwix Serve

本文软件是网友 刘源 推荐的&#xff0c;因为他已经安装成功了&#xff0c;所以老苏拖拖拉拉的就从去年拖到了现在&#xff1b; &#x1f602; 什么是 Kiwix ? Kiwix 是一个用于浏览离线内容的自由开源浏览器&#xff0c;最初用于离线浏览维基百科。Kiwix 可以读取以压缩形式存…

[神经网络]基干网络之VGG、ShuffleNet

一、VGG VGG是传统神经网络堆叠能达到的极限深度。 VGG分为VGG16和VGG19&#xff0c;其均有以下特点&#xff1a; ①按2x2的Pooling层&#xff0c;网络可以分成若干段 ②每段之内由若干same卷积操作构成&#xff0c;段内Feature Map数量固定不变&#xff1b; ③Feature Map按2的…

对个人博客系统进行web自动化测试(包含测试代码和测试的详细过程)

目录 一、总述 二、登录页面测试 一些准备工作 验证页面显示是否正确 验证正常登录的情况 该过程中出现的问题 验证登录失败的情况 关于登录界面的总代码 测试视频 三、注册界面的自动化测试 测试代码 过程中出现的bug 测试视频 四、博客列表页测试&#xff08;…

【Leedcode】数据结构中链表必备的面试题(第四期)

【Leedcode】数据结构中链表必备的面试题&#xff08;第四期&#xff09; 文章目录【Leedcode】数据结构中链表必备的面试题&#xff08;第四期&#xff09;1.题目2.思路图解(1)思路一(2)思路二3.源代码总结1.题目 相交链表&#xff1a; 如下&#xff08;示例&#xff09;&…

小白福利!我开发了一个快速部署库

1、开发背景 很多入门的同学&#xff0c;在跟着视频敲完代码之后&#xff0c;在打包出来的产物犯了难 如果是 hash 路由&#xff0c;要么使用后端部署&#xff0c;要么使用 github 或者 gitee 提供的静态部署服务如果是 history 路由&#xff0c;那只能使用后端框架进行部署&a…

内网渗透(五十三)之域控安全和跨域攻击-利用域信任密钥获取目标域控

系列文章第一章节之基础知识篇 内网渗透(一)之基础知识-内网渗透介绍和概述 内网渗透(二)之基础知识-工作组介绍 内网渗透(三)之基础知识-域环境的介绍和优点 内网渗透(四)之基础知识-搭建域环境 内网渗透(五)之基础知识-Active Directory活动目录介绍和使用 内网渗透(六)之基…

前端学习日记——Vue之Vuex初识(一)

前言 学习前端一段时间了&#xff0c;因为一直是做Python开发&#xff0c;所以凭借着语言的通性学习Javascript、Vue轻快很多&#xff0c;但一些碎片化的知识及插件的使用方法还是需要记录一下&#xff0c;时而复习&#xff0c;形成系统化的知识体系&#xff08;PS&#xff1a;…

【Linux线程池】

Linux线程池Linux线程池线程池的概念线程池的优点线程池的应用场景线程池的实现Linux线程池 线程池的概念 线程池是一种线程使用模式。 线程过多会带来调度开销&#xff0c;进而影响缓存局部和整体性能&#xff0c;而线程池维护着多个线程&#xff0c;等待着监督管理者分配可并…

JavaScript if…else 语句

条件语句用于基于不同的条件来执行不同的动作。条件语句通常在写代码时&#xff0c;您总是需要为不同的决定来执行不同的动作。您可以在代码中使用条件语句来完成该任务。在 JavaScript 中&#xff0c;我们可使用以下条件语句&#xff1a;if 语句 - 只有当指定条件为 true 时&a…

【企业云端全栈开发实践-3】Spring Boot文件上传服务+拦截器

本节目录一、静态资源访问二、文件上传原理三、拦截器3.1 拦截器定义代码3.2 拦截器注册一、静态资源访问 使用IDEA创建Spring Boot项目时&#xff0c;会默认创建classpath://static/目录&#xff0c;静态资源一般放在这个目录下即可。 如果默认的静态资源过滤策略不能满足开…

做独立开发者,能在AppStore赚到多少钱?

成为一名独立开发者&#xff0c;不用朝九晚五的上班&#xff0c;开发自己感兴趣的产品&#xff0c;在AppStore里赚美金&#xff0c;这可能是很多程序员的梦想&#xff0c;今天就来盘一盘&#xff0c;这个梦想实现的概率有多少。 先来了解一些数据&#xff1a; 2022年5月26日&am…

目标跟踪系列总结

目标跟踪算法&#xff1a; sort算法: sort算法流程图 关联成功的检测box与追踪box处理&#xff1a;使用检测的box对追踪结果进行KalmanFilter权重以及参数更新&#xff0c;同时记录关联追踪box的计数次数&#xff1b; 未关联成功的box处理&#xff1a;对检测的box进行KalmanF…

C++【内存管理】

文章目录C内存管理一、C/C内存分布1.1.C/C内存区域划分图解&#xff1a;1.2.根据代码进行内存区域分析二、C内存管理方式2.1.new/delete操作内置类型2.2.new和delete操作自定义类型三、operator new与operator delete函数四、new和delete的实现原理4.1.内置类型4.2.自定义类型4…

如何利用有限的数据发表更多的SCI论文?——利用ArcGIS探究环境和生态因子对水体、土壤和大气污染物的影响

SCI的写作和发表是科研人提升自身实力和实现自己价值的必要途径。“如何利用有限的数据发表更多的SCI论文&#xff1f;”是我们需要解决的关键问题。软件应用只是过程和手段&#xff0c;理解事件之间的内在逻辑和寻找事物之间的内在规律才是目的。如何利用有限的数据发表更多的…

互联网企业如何进行数字化转型?业务需求迭代频繁的应对之策!

互联网行业作为我国数字经济发展“四化”框架中生产力主要组成部分&#xff0c;是国家数字化转型的主要推动者之一。为此&#xff0c;相对于其他传统行业来说&#xff0c;互联网行业企业数字化转型的紧迫程度更高&#xff0c;如果不数字化转型或者转型不成功&#xff0c;会有更…

ArcGIS制作地形分析

ArcGIS制作地形分析的方法解析 树谷资料库资源大全&#xff08;2月9日更新&#xff09; 在地形变化较大的建筑、景观、城市设计项目中&#xff0c;高程、坡度、坡向分析是非常重要的&#xff0c;而在这几类分析中&#xff0c;ArcGIS软件可以比较方便的完成相关分析的制作。今…

OAuth2.0入门

什么是OAuth2.0 OAuth&#xff08;Open Authorization&#xff09;是一个关于授权&#xff08;authorization&#xff09;的开放网络标准&#xff0c;允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息&#xff0c;而不需要将用户名和密码提供给第三方移动应用或…

HTML基础(3)

HTML基础单选框、复选框、下拉框文本框< script >标签属性< script >基本使用单选框、复选框、下拉框 文本框 < script >标签属性 type属性定义script元素包含或src引用的脚本语言。属性值是MIME类型&#xff0c;包括text/javascript,text/ecmascript, appl…

SpringBoot2零基础到项目实战-基础篇

SSM内容01-SpringBoot工程入门案例开发步骤SpringBoot 是 Pivotal 团队提供的全新框架&#xff0c;设计目的是简化 Spring 应用的初始搭建以及开发过程。使用了 Spring 框架后已经简化了我们的开发。而 SpringBoot 又是对 Spring 开发进行简化的&#xff0c;可想而知 SpringBoo…

使用DQN进行价格管理

文章目录前言一、不同的价格响应二、利用DQN优化定价策略1.定义环境2.DQN算法概述3.Algorithm: Deep Q Network (DQN)总结强化学习-定价、决策前言 供应链和价格管理是企业运营中最早采用数据科学和组合优化方法的领域&#xff0c;并且在使用这些技术方面有着悠久的历史&#…