分享一下筆者研讀ClickHouse原始碼時分析函式呼叫的實作,重點在于分析Clickhouse查詢層實作的介面,以及Clickhouse是如何利用這些介面更好的實作向量化的,本文的原始碼分析基于ClickHouse v19.16.2.2的版本,
1.舉個栗子
下面是一個簡單的SQL陳述句
SELECT a, abs(b) FROM test
這里呼叫一個abs的函式,我們先打開ClickHouse的Debug日志看一下執行計劃,(當前ClickHouse不支持使用Explain陳述句來查看執行計劃,這個確實是很蛋疼的~~)

這里分為了3個流
- ExpressionBlockInputStream: 最頂層的Expression,實作了Projection,這個和我們今天主題無關,本質上就是實作一個簡單列的改名操作,比如
select a as aaa from test這里將列名從a改為aaa. - ExpressionBlockInputStream: 第二個ExpressionBlockInputStream就是我們關注的重點的,后面的章節會詳細的剖析它,它主要完成了下面兩件事情
-
- 對
b列執行函式abs,生成新的一列資料abs(b)
- 對
-
remove column b, 將b列洗掉,新的Block為a, abs(b)
-
- TinyLogBlockInputStream: 存盤引擎的讀取流,這里標識了底層表的存盤引擎為
append only的TinyLog,
從上面的執行計劃可以看出,Clickhouse的運算式計算是由ExpressionBlockInputStream來完成的,而這個類是一個很強大的類,可以實作:Projection, Join, Apply_Function, Add Column, Remove Column等,
2. 實作流程的梳理
- ExpressionBlockInputSteam readImpl()的實作
直接上代碼,看一下ExpressionBlockInputStream的讀取方法的實作
Block ExpressionBlockInputStream::readImpl()
{
Block res = children.back()->read();
if (res)
expression->execute(res);
return res;
}
這里的實作很簡單,就是不停從底層的流讀取資料Block,Block可以理解為Doris之中的Batch,相當一組資料,然后在Block之上執行運算式計算,之后回傳給上節點,所以這里的重點就在于運算式計算的實作類ExpressionActions的指標expression,它封裝了一組運算式的Action,在Block上依次執行這些Action,
- Action excute的實作
Action支持多種操作,包含了:
enum Type {
ADD_COLUMN,
REMOVE_COLUMN,
COPY_COLUMN,
APPLY_FUNCTION,
ARRAY_JOIN,
JOIN,
PROJECT,
ADD_ALIASES,
};
這里我們重點關注的是函式執行的實作,可以直接定位到APPLY_FUNCTION的代碼:
case APPLY_FUNCTION:
{
1. 從Block之中篩選出對應的引數陣列
ColumnNumbers arguments(argument_names.size());
for (size_t i = 0; i < argument_names.size(); ++i)
{
arguments[i] = block.getPositionByName(argument_names[i]);
}
2.新建一個結果的列,對應函式的結果會寫入結果列,把結果列寫入的Block之中
size_t num_columns_without_result = block.columns();
block.insert({ nullptr, result_type, result_name});
3.呼叫對應的函式指標,執行函式呼叫
function->execute(block, arguments, num_columns_without_result, input_rows_count, dry_run);
這里我保留一部分關鍵的執行路徑代碼,并添加了對應的中文注釋,
選出了函式執行的引數,并添加了新的一個空列用于存盤函式abs(b)的最終結果,新的列的偏移量就是num_columns_without_result指定的,

接下來這里我們這里重點關注Function的execute介面的引數就可以了:
- block:實際存盤的資料
- arguments:列的引數偏移量
- num_columns_without_result:函式計算結果的寫入列
- input_rows_count: block之中的資料行數
這里本質上是呼叫了介面IFunction的介面,它的子類需要實作對應的excuteImpl的方法:
class IFunction : public std::enable_shared_from_this<IFunction>,
public FunctionBuilderImpl, public IFunctionBase, public PreparedFunctionImpl
{
public:
/// TODO: make const
void executeImpl(Block & block, const ColumnNumbers & arguments, size_t result, size_t input_rows_count) override = 0;
而最終的實作是IFunction的子類:FunctionUnaryArithmetic實作了該方法,該方法的核心代碼如下:
if (auto col = checkAndGetColumn<ColumnVector<T0>>(block.getByPosition(arguments[0]).column.get()))
{
auto col_res = ColumnVector<typename Op<T0>::ResultType>::create();
auto & vec_res = col_res->getData();
vec_res.resize(col->getData().size());
UnaryOperationImpl<T0, Op<T0>>::vector(col->getData(), vec_res);
block.getByPosition(result).column = std::move(col_res);
return true;
}
這里最為核心的是,將arguments的列作為引數列取出為變數col, 而col_res創建了個新的列,存放result的結果,這里最重要的方法就是
UnaryOperationImpl<T0, Op<T0>>::vector,從名字上也能看出,它實作了函式的向量化計算,我們繼續看這部分代碼:
static void NO_INLINE vector(const ArrayA & a, ArrayC & c)
{
size_t size = a.size();
for (size_t i = 0; i < size; ++i)
c[i] = Op::apply(a[i]);
}
顯然,這就是一個完美的向量化優化代碼,沒有任何if, switch, break的分支跳轉陳述句,for回圈的長度也是已知的,這里的Op::apply就是咱們呼叫的AbsImpl::apply函式的實作:
template <typename A>
struct AbsImpl
{
static inline NO_SANITIZE_UNDEFINED ResultType apply(A a)
{
if constexpr (IsDecimalNumber<A>)
return a < 0 ? A(-a) : a;
else if constexpr (std::is_integral_v<A> && std::is_signed_v<A>)
return a < 0 ? static_cast<ResultType>(~a) + 1 : a;
else if constexpr (std::is_integral_v<A> && std::is_unsigned_v<A>)
return static_cast<ResultType>(a);
else if constexpr (std::is_floating_point_v<A>)
return static_cast<ResultType>(std::abs(a));
}
走的這里,相當于走完了整個函式呼叫的流程,而其他多引數的函式的實作也是大同小異,如:
struct BinaryOperationImplBase
{
using ResultType = ResultType_;
static void NO_INLINE vector_vector(const PaddedPODArray<A> & a, const PaddedPODArray<B> & b, PaddedPODArray<ResultType> & c)
{
size_t size = a.size();
for (size_t i = 0; i < size; ++i)
c[i] = Op::template apply<ResultType>(a[i], b[i]);
}
而執行完成abs(b)函式之后,b列就沒有用處了,Clickhouse會呼叫另一個Action:REMOVE_COLUM在Block之中洗掉b列,這樣就得到了我們所需要的兩個列a, abs(b)組成的新的Block,

3.要點梳理
第二小節梳理完成了一整個函式呼叫的流程,這里重點梳理一下實作向量化函式調要點:
- ClickHouse的計算是純粹函式式編程式的計算,不會改變原先的列狀態,而是產生一組新的列,
- 各個函式的實作需要繼承IFunction的介面,實作
execute的方法,該方法基于Block進行執行, - 最終繼承IFunction介面的實作類都需要override的
execute方法,并真正實作對應的函式vectoer的呼叫,這里Clickhouse確保了For回圈的長度是已知的,同時沒有對應跳轉陳述句,確保了編譯器進行向量化優化時有足夠的親和度,(這里可以打開gcc的編譯flag:-fopt-info-vec或者clang的編譯選項:-Rpass=loop-vectorize來查看實際源代碼的向量化情況)
4. 小結
好了,到這里也就把ClickHouse函式呼叫的代碼梳理完了,
除了abs函式外,其他的函式的執行也是同樣通過類似的方式依次來實作和處理的,原始碼閱讀的步驟也可以參照筆者的分析流程來參考,
筆者是一個ClickHouse的初學者,對ClickHouse有興趣的同學,歡迎多多指教,交流,
5. 參考資料
官方檔案
ClickHouse源代碼
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/262442.html
標籤:其他
