由于工作的需求,后续笔者工作需要和开源的OLAP数据库ClickHouse打交道。ClickHouse是Yandex在2016年6月15日开源了一个分析型数据库,以强悍的单机处理能力被称道

笔者在实际测试ClickHouse和阅读ClickHouse的源码过程之中,对"战斗民族"开发的数据库十分欣赏。ClickHouse不仅是一个很好的数据库学习材料,而且同时应用了大量的CPP17的新特性进行开发,也是一个大型的Modern CPP的教导资料。

笔者接下来会陆续将阅读ClickHouse的部分心得体会与通过源码阅读笔记的方式和大家分享,坦白说,这种源码阅读笔记很难写啊。(多一分繁琐,少一分就模糊了~~)

第一篇文章,我们就从聚合函数的实现开始聊起~~ 上车!

1.基础知识的梳理

什么是聚合函数?

聚合函数: 顾名思义就是对一组数据执行聚合计算并返回结果的函数。

这类函数在数据库之中很常见,如:count, max, min, sum等等。

ClickHouse的实现接口
  • IAggregateFunction接口

    在ClickHouse之中,定义了一个统一的聚合函数接口:IAggregateFunction.(在ClickHouse之中,所有的接口类都是以大写的I开头的。) 上文笔者提到的聚合函数,则都是作为抽象类IAggregateFunction的子类实现的。其中该接口最为核心的方法是下面这5个方法:

    • add函数:最为核心的调用接口,将对应AggregateDataPtr指针之中数据取出,与列columns中的第row_num的数据进行对应的聚合计算。(这里可以看到ClickHouse是一个纯粹的列式存储数据库,所有的操作都是基于列的数据结构。)
    • merge函数:将两个聚合结果进行合并的函数,通常用在并发执行聚合函数的过程之中,需要将对应的聚合结果进行合并。
    • serialize函数与deserialize函数:序列化与反序列化的函数,通常用于spill to disk或分布式场景需要保存或传输中间结果的。
    • addBatch函数:这是函数也是非常重要的,虽然它仅仅实现了一个for循环调用add函数。它通过这样的方式来减少虚函数的调用次数,并且增加了编译器内联的概率。(虚函数的调用需要一次访存指令,一次查表,最终才能定位到需要调用的函数上,这在传统的火山模型的实现上会带来极大的CPU开销。
  /** Adds a value into aggregation data on which place points to.
* columns points to columns containing arguments of aggregation function.
* row_num is number of row which should be added.
* Additional parameter arena should be used instead of standard memory allocator if the addition requires memory allocation.
*/
virtual void add(AggregateDataPtr place, const IColumn ** columns, size_t row_num, Arena * arena) const = 0; /// Merges state (on which place points to) with other state of current aggregation function.
virtual void merge(AggregateDataPtr place, ConstAggregateDataPtr rhs, Arena * arena) const = 0; /// Serializes state (to transmit it over the network, for example).
virtual void serialize(ConstAggregateDataPtr place, WriteBuffer & buf) const = 0; /// Deserializes state. This function is called only for empty (just created) states.
virtual void deserialize(AggregateDataPtr place, ReadBuffer & buf, Arena * arena) const = 0;
// /** Contains a loop with calls to "add" function. You can collect arguments into array "places"
* and do a single call to "addBatch" for devirtualization and inlining.
*/
virtual void addBatch(size_t batch_size, AggregateDataPtr * places, size_t place_offset, const IColumn ** columns, Arena * arena) const = 0;
  • 抽象类IColumn

    上面的接口IAggregateFunction的函数使用到了ClickHouse的核心接口IColumn类,这里也进行简要的介绍。 IColumn 接口表达了所有数据在ClickHouse之中的用内存表达的数据结构,其他带有具体数据类型的如ColumnUInt8、ColumnArray 等, 都实现了对应的列接口,并且在子类之中具象实现了不同的内存布局。

    IColumn的子类实现细节很琐碎,笔者这里就暂时不展开讲了,笔者这里就简单讲讲涉及到聚合函数调用部分的IColumn接口的对应方法:

    这里columns是一个二维数组,通过columns[0]可以取到第一列。(这里只有涉及到一列,为什么columns是二维数组呢?因为处理array等列的时候,也是通过对应的接口,而array就需要应用二维数组了. )

    注意这里有一个强制的类型转换,column已经转换为ColVecType类型了,这是模板派生出IColumn的子类。

    然后通过IColumn子类实现的getData方法获取对应row_num行的数据进行add函数调用就完成了一次聚合函数的计算了。
    void add(AggregateDataPtr place, const IColumn ** columns, size_t row_num, Arena *) const override
{
const auto & column = static_cast<const ColVecType &>(*columns[0]);
this->data(place).add(column.getData()[row_num]);
}
  • IAggregateFunctionHelper接口

    这个接口是上面提到 IAggregateFunction的辅助子类接口,它很巧妙的通过模板的类型派生,将虚函数的调用转换为函数指针的调用,这个在实际聚合函数的实现过程之中能够大大提高计算的效率。

    函数addFree就实现了我上述所说的过程,但是它是一个private的函数,所以通常我们都是通过getAddressOfAddFunction获取对应的函数地址。这在聚合查询的过程之中能够提高20%左右的执行效率。
template <typename Derived>
class IAggregateFunctionHelper : public IAggregateFunction
{
private:
static void addFree(const IAggregateFunction * that, AggregateDataPtr place, const IColumn ** columns, size_t row_num, Arena * arena)
{
static_cast<const Derived &>(*that).add(place, columns, row_num, arena);
} public:
IAggregateFunctionHelper(const DataTypes & argument_types_, const Array & parameters_)
: IAggregateFunction(argument_types_, parameters_) {} AddFunc getAddressOfAddFunction() const override { return &addFree; }
  • AggregateFunctionFactory类

    顾名思义,这个是一个生成聚合函数的工厂类。它的逻辑很简单,所有ClickHouse之中所相关的聚合函数都是通过这个工厂类注册并且获取,然后进行调用的。
class AggregateFunctionFactory final : private boost::noncopyable, public IFactoryWithAliases<AggregateFunctionCreator>
{
public: static AggregateFunctionFactory & instance(); /// Register a function by its name.
/// No locking, you must register all functions before usage of get.
void registerFunction(
const String & name,
Creator creator,
CaseSensitiveness case_sensitiveness = CaseSensitive); /// Throws an exception if not found.
AggregateFunctionPtr get(
const String & name,
const DataTypes & argument_types,
const Array & parameters = {},
int recursion_level = 0) const;

2.聚合函数的注册流程

有了上述的背景知识,我们接下来举个栗子。来看看一个聚合函数的实现细节,以及它是如何被使用的。

AggregateFunctionSum

笔者这里选取了一个很简单的聚合算子Sum,我们来看看它实现的代码细节。

这里我们可以看到AggregateFunctionSum是个final类,无法被继承了。而它继承了上面提到的IAggregateFunctionHelp类的子类IAggregateFunctionDataHelper类。

这里我们就重点看,这个类override了getName方法,返回了对应的名字sum。并且实现了我们上文提到的四个核心的方法。

  • add
  • merge
  • seriable
  • deserialize
template <typename T, typename TResult, typename Data>
class AggregateFunctionSum final : public IAggregateFunctionDataHelper<Data, AggregateFunctionSum<T, TResult, Data>>
{
public:
using ResultDataType = std::conditional_t<IsDecimalNumber<T>, DataTypeDecimal<TResult>, DataTypeNumber<TResult>>;
using ColVecType = std::conditional_t<IsDecimalNumber<T>, ColumnDecimal<T>, ColumnVector<T>>;
using ColVecResult = std::conditional_t<IsDecimalNumber<T>, ColumnDecimal<TResult>, ColumnVector<TResult>>; String getName() const override { return "sum"; } AggregateFunctionSum(const DataTypes & argument_types_)
: IAggregateFunctionDataHelper<Data, AggregateFunctionSum<T, TResult, Data>>(argument_types_, {})
, scale(0)
{} AggregateFunctionSum(const IDataType & data_type, const DataTypes & argument_types_)
: IAggregateFunctionDataHelper<Data, AggregateFunctionSum<T, TResult, Data>>(argument_types_, {})
, scale(getDecimalScale(data_type))
{} DataTypePtr getReturnType() const override
{
if constexpr (IsDecimalNumber<T>)
return std::make_shared<ResultDataType>(ResultDataType::maxPrecision(), scale);
else
return std::make_shared<ResultDataType>();
} void add(AggregateDataPtr place, const IColumn ** columns, size_t row_num, Arena *) const override
{
const auto & column = static_cast<const ColVecType &>(*columns[0]);
this->data(place).add(column.getData()[row_num]);
} void merge(AggregateDataPtr place, ConstAggregateDataPtr rhs, Arena *) const override
{
this->data(place).merge(this->data(rhs));
} void serialize(ConstAggregateDataPtr place, WriteBuffer & buf) const override
{
this->data(place).write(buf);
} void deserialize(AggregateDataPtr place, ReadBuffer & buf, Arena *) const override
{
this->data(place).read(buf);
} void insertResultInto(ConstAggregateDataPtr place, IColumn & to) const override
{
auto & column = static_cast<ColVecResult &>(to);
column.getData().push_back(this->data(place).get());
} private:
UInt32 scale;
};

接下来,ClickHouse实现了两种聚合计算:AggregateFunctionSumDataAggregateFunctionSumKahanData。后者是用Kahan算法避免float类型精度损失的,我们可以暂时不细看。直接看SumData的实现。这是个模板类,之前我们讲到AggregateFunction的函数就是通过AggregateDataPtr指针来获取AggregateFunctionSumData的地址,来调用add实现聚合算子的。我们可以看到AggregateFunctionSumData实现了前文提到的add, merge, write,read四大方法,正好和接口一一对应上了。

template <typename T>
struct AggregateFunctionSumData
{
T sum{}; void add(T value)
{
sum += value;
} void merge(const AggregateFunctionSumData & rhs)
{
sum += rhs.sum;
} void write(WriteBuffer & buf) const
{
writeBinary(sum, buf);
} void read(ReadBuffer & buf)
{
readBinary(sum, buf);
} T get() const
{
return sum;
}
};

ClickHouse在Server启动时。main函数之中会调用registerAggregateFunction的初始化函数注册所有的聚合函数。

然后调用到下面的函数:

void registerAggregateFunctionSum(AggregateFunctionFactory & factory)
{
factory.registerFunction("sum", createAggregateFunctionSum<AggregateFunctionSumSimple>, AggregateFunctionFactory::CaseInsensitive);
factory.registerFunction("sumWithOverflow", createAggregateFunctionSum<AggregateFunctionSumWithOverflow>);
factory.registerFunction("sumKahan", createAggregateFunctionSum<AggregateFunctionSumKahan>);
}

这里又调用了 factory.registerFunction("sum", createAggregateFunctionSum<AggregateFunctionSumSimple>, AggregateFunctionFactory::CaseInsensitive);来进行上述我们看到的聚合函数的注册。这里有一点很恶心的模板代码,笔者这里简化了一下,把注册的部分函数拉出来:

createAggregateFunctionSum(const std::string & name, const DataTypes & argument_types, const Array & parameters)
{
AggregateFunctionPtr res;
DataTypePtr data_type = argument_types[0];
if (isDecimal(data_type))
res.reset(createWithDecimalType<Function>(*data_type, *data_type, argument_types));
else
res.reset(createWithNumericType<Function>(*data_type, argument_types));
return res;

这里的Function模板就是上面的AggregateFunctionSumSimple, 而它又是下面的模板类型:

template <typename T> using AggregateFunctionSumSimple = typename SumSimple<T>::Function;

template <typename T>
struct SumSimple
{
/// @note It uses slow Decimal128 (cause we need such a variant). sumWithOverflow is faster for Decimal32/64
using ResultType = std::conditional_t<IsDecimalNumber<T>, Decimal128, NearestFieldType<T>>;
using AggregateDataType = AggregateFunctionSumData<ResultType>;
using Function = AggregateFunctionSum<T, ResultType, AggregateDataType>;
};

不知道读者被绕晕了没,最终绕回来还是new出来这个AggregateFunctionSum<T, ResultType, AggregateDataType>

也就是完成了这个求和算子的注册,后续我们get出来就可以愉快的调用啦。(这里这部分的模板变化比较复杂,如果看不明白可以回到源码梳理一下~~~)

3. 小结

好了,关于聚合函数的基础信息,和它是如何实现并且通过工厂方法注册获取的流程算是搞明白了。

关于其他的聚合算子,也是大同小异的方式。笔者就不再赘述了,感兴趣的可以回到源码之中继续一探究竟。讲完了聚合函数的实现,下一篇笔者就要继续给探究聚合函数究竟在ClickHouse之中是如何和列存结合使用,并实现向量化的~~。

笔者是一个ClickHouse的初学者,对ClickHouse有兴趣的同学,也欢迎和笔者多多指教,交流。

4. 参考资料

官方文档

ClickHouse源代码

ClickHouse源码笔记1:聚合函数的实现的更多相关文章

  1. ClickHouse源码笔记5:聚合函数的源码再梳理

    笔者在源码笔记1之中分析过ClickHouse的聚合函数的实现,但是对于各个接口函数的实际如何共同工作的源码,回头看并没有那么明晰,主要原因是没有结合Aggregator的类来一起分析聚合函数的是如果 ...

  2. ClickHouse源码笔记2:聚合流程的实现

    上篇笔记讲到了聚合函数的实现并且带大家看了聚合函数是如何注册到ClickHouse之中的并被调用使用的.这篇笔记,笔者会续上上篇的内容,将剖析一把ClickHouse聚合流程的整体实现. 第二篇文章, ...

  3. ClickHouse源码笔记4:FilterBlockInputStream, 探寻where,having的实现

    书接上文,本篇继续分享ClickHouse源码中一个重要的流,FilterBlockInputStream的实现,重点在于分析Clickhouse是如何在执行引擎实现向量化的Filter操作符,而利用 ...

  4. ClickHouse源码笔记3:函数调用的向量化实现

    分享一下笔者研读ClickHouse源码时分析函数调用的实现,重点在于分析Clickhouse查询层实现的接口,以及Clickhouse是如何利用这些接口更好的实现向量化的.本文的源码分析基于Clic ...

  5. ClickHouse源码笔记6:探究列式存储系统的排序

    分析完成了聚合以及向量化过滤,向量化的函数计算之后.本篇,笔者将分析数据库的一个重要算子:排序.让我们从源码的角度来剖析ClickHouse作为列式存储系统是如何实现排序的. 本系列文章的源码分析基于 ...

  6. clickhouse源码Redhat系列机单机版安装踩坑笔记

    前情概要 由于工作需要用到clickhouse, 这里暂不介绍概念,应用场景,谷歌,百度一大把. 将安装过程踩下的坑记录下来备用 ClickHouse源码 git clone安装(直接下载源码包安装失 ...

  7. jQuery源码笔记(一):jQuery的整体结构

    jQuery 是一个非常优秀的 JS 库,与 Prototype,YUI,Mootools 等众多的 Js 类库相比,它剑走偏锋,从 web 开发的实用角度出发,抛除了其它 Lib 中一些中看但不实用 ...

  8. Zepto源码笔记(一)

    最近在研究Zepto的源码,这是第一篇分析,欢迎大家继续关注,第一次写源码笔记,希望大家多指点指点,第一篇文章由于首次分析原因不会有太多干货,希望后面的文章能成为各位大大心目中的干货. Zepto是一 ...

  9. redis源码笔记(一) —— 从redis的启动到command的分发

    本作品采用知识共享署名 4.0 国际许可协议进行许可.转载联系作者并保留声明头部与原文链接https://luzeshu.com/blog/redis1 本博客同步在http://www.cnblog ...

随机推荐

  1. Redis实现分布式锁(设计模式应用实战)

    笔者看过网络上各种各样使用redis实现分布式锁的代码,要么错误,要么片段化,没有一个完整的例子,借这个周末给大家总结一下redis实现分布式锁的两种机制 自旋锁和排他锁 鉴于实现锁的方式不同,那么这 ...

  2. [hdu1506 Largest Rectangle in a Histogram]笛卡尔树

    题意:http://acm.hdu.edu.cn/showproblem.php?pid=1506 如图,求最大的矩形面积 思路: 笛卡尔树:笛卡尔树是一棵二叉树,树的每个节点有两个值,一个为key, ...

  3. [hdu3572]最大流(dinic)

    题意:有m台机器,n个任务,每个任务需要在第si~ei天之间,且需要pi天才能完成,每台机器每天只能做一个任务,不同机器每天不能做相同任务,判断所有任务是否可以做完. 思路: 把影响答案的对象提取出来 ...

  4. 基于ELK搭建MySQL日志平台的要点和常见错误

    第一部分 概括 ELK是集分布式数据存储.可视化查询和日志解析于一体的日志分析平台.ELK=elasticsearch+Logstash+kibana,三者各司其职,相互配合,共同完成日志的数据处理工 ...

  5. IOS App打包发布完整流程

    注册成为开发者 登录苹果开发者中心,点击Accounts,在这里需要你填写你的Appple ID进行登录,如果没有,点击这里申请一个,填写信息就成,这里就不再赘述.申请完成之后,使用申请的AppID进 ...

  6. python 一个模块找不到的错误:ModuleNotFoundError

    阿刁是一个自动化测试用例,从一出生他就被赋予终生使命,去测试一个叫登录的过程是否合理.他一直就被关在一个小黑屋里面,从来也没有出去过,小黑屋里还被关着其他的同胞,他们身上都捆着两个小袋子. 小黑屋里很 ...

  7. UEFI Shell --常用命令解释

    UEFI Shell解释 UEFI Shell 是一个提供用户和UEFI系统之间的接口,进入UEFI Shell可以对计算机系统进行配置 命令解释: 单独的help就可以输出所有指令,不做特殊说明,内 ...

  8. CSS之未知高度img垂直居中

    测试代码如下:(能够水平居中,通过text-align:center实现) <style>.box{ width:800px;height:600px;border:2px solid # ...

  9. 如何在最新版本的eclipse上使用低版本的jdk

    高版本的eclipse在第一次打开的时候只能配置相匹配的jdk.例如我所示版本eclipse版本初始化的时候提示要jdk1.8版本才能打开,可是根据实际工作情况需要jdk1.7. 我的eclipse版 ...

  10. 最短路径——floyd算法代码(c语言)

    最短路径问题 昨天自己试了试写一下dijkstra的算法博客 dijkstra链接在这← 今天来更floyd算法,感觉非常简单果然暴力才是解决一切的王道 一.总体思想 floyd算法就是每一次从邻接矩 ...