诺禾-ClickHouse

ClickHouse源码笔记1:聚合函数的完成
由于工作的需求,后续笔者工作需求和开源的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(*columns[0]);
this->data(place).add(column.getData()[row_num]);
}
IAggregateFunctionHelper接口
这个接口是上面提到 IAggregateFunction的辅助子类接口,它很巧妙的经过模板的类型派生,将虚函数的调用转换为函数指针的调用,这个在实践聚合函数的完成过程之中可以大大进步计算的效率。
函数addFree就完成了我上述所说的过程,但是它是一个private的函数,所以通常我们都是经过getAddressOfAddFunction获取对应的函数地址。这在聚合查询的过程之中可以进步20%左右的执行效率。
template
class IAggregateFunctionHelper : public IAggregateFunction
{
private:
static void addFree(const IAggregateFunction * that, AggregateDataPtr place, const IColumn ** columns, size_t row_num, Arena * arena)
{
static_cast(*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
{
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
class AggregateFunctionSum final : public IAggregateFunctionDataHelper>
{
public:
using ResultDataType = std::conditional_t;
using ColVecType = std::conditional_t;
using ColVecResult = std::conditional_t;
String getName() const override { return “sum”; }
AggregateFunctionSum(const DataTypes & argument_types_)
: IAggregateFunctionDataHelper>(argument_types_, {})
, scale(0)
{}
AggregateFunctionSum(const IDataType & data_type, const DataTypes & argument_types_)
: IAggregateFunctionDataHelper>(argument_types_, {})
, scale(getDecimalScale(data_type))
{}
DataTypePtr getReturnType() const override
{
if constexpr (IsDecimalNumber)
return std::make_shared(ResultDataType::maxPrecision(), scale);
else
return std::make_shared();
}
void add(AggregateDataPtr place, const IColumn ** columns, size_t row_num, Arena *) const override
{
const auto & column = static_cast(*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(to);
column.getData().push_back(this->data(place).get());
}
private:
UInt32 scale;
};
接下来,ClickHouse完成了两种聚合计算:AggregateFunctionSumData和AggregateFunctionSumKahanData。后者是用Kahan算法防止float类型精度损失的,我们能够暂时不细看。直接看SumData的完成。这是个模板类,之前我们讲到AggregateFunction的函数就是经过AggregateDataPtr指针来获取AggregateFunctionSumData的地址,来调用add完成聚合算子的。我们能够看到AggregateFunctionSumData完成了前文提到的add, merge, write,read四大办法,正好和接口逐个对应上了。

template
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, AggregateFunctionFactory::CaseInsensitive);
factory.registerFunction(“sumWithOverflow”, createAggregateFunctionSum);
factory.registerFunction(“sumKahan”, createAggregateFunctionSum);
}
这里又调用了factory.registerFunction(“sum”, createAggregateFunctionSum, 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(*data_type, *data_type, argument_types));
else
res.reset(createWithNumericType(*data_type, argument_types));
return res;
这里的Function模板就是上面的AggregateFunctionSumSimple, 而它又是下面的模板类型:

template using AggregateFunctionSumSimple = typename SumSimple::Function;
template
struct SumSimple
{
/// @note It uses slow Decimal128 (cause we need such a variant). sumWithOverflow is faster for Decimal32/64
using ResultType = std::conditional_t;
using AggregateDataType = AggregateFunctionSumData;
using Function = AggregateFunctionSum;
};
不晓得读者被绕晕了没,最终绕回来还是new出来这个AggregateFunctionSum
也就是完成了这个求和算子的注册,后续我们get出来就能够高兴的调用啦。(这里这局部的模板变化比拟复杂,假如看不明白能够回到源码梳理一下~~~)

  1. 小结
    好了,关于聚合函数的根底信息,和它是如何完成并且经过工厂办法注册获取的流程算是搞明白了。
    关于其他的聚合算子,也是大同小异的方式。笔者就不再赘述了,感兴味的能够回到源码之中继续一探求竟。讲完了聚合函数的完成,下一篇笔者就要继续给探求聚合函数终究在ClickHouse之中是如何和列存分离运用,并完成向量化的~~。
    笔者是一个ClickHouse的初学者,对ClickHouse有兴味的同窗,也欢送和笔者多多指教,交流。

你可能感兴趣的:(编程语言)