1. 程式人生 > >ClickHouse原始碼筆記3:函式呼叫的向量化實現

ClickHouse原始碼筆記3:函式呼叫的向量化實現

>分享一下筆者研讀ClickHouse原始碼時分析函式呼叫的實現,重點在於分析Clickhouse查詢層實現的介面,以及Clickhouse是如何利用這些介面更好的實現向量化的。本文的原始碼分析基於ClickHouse v19.16.2.2的版本。 ### 1.舉個栗子 下面是一個簡單的SQL語句 ```SELECT a, abs(b) FROM test``` 這裡呼叫一個abs的函式,我們先開啟ClickHouse的Debug日誌看一下執行計劃。(當前ClickHouse不支援使用**Explain語句**來檢視執行計劃,這個確實是很蛋疼的~~) ![ClickHouse的執行PipeLine](https://upload-images.jianshu.io/upload_images/8552201-c5ca4ff4160e381f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 這裡分為了3個流 * ExpressionBlockInputStream: 最頂層的Expression,實現了Projection,這個和我們今天主題無關,本質上就是實現一個簡單列的改名操作。比如 `select a as aaa from test`這裡將列名從`a`改為`aaa`. * ExpressionBlockInputStream: 第二個ExpressionBlockInputStream就是我們關注的重點的,後面的章節會詳細的剖析它。它主要完成了下面兩件事情 * 1. 對`b`列執行函式`abs`,生成新的一列資料`abs(b)` * 2. `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`指定的。 ![添加了新的一個空列](https://upload-images.jianshu.io/upload_images/8552201-30915805a6f535c5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 接下來這裡我們這裡重點關注Function的execute介面的引數就可以了: * **block**:實際儲存的資料 * **arguments**:列的引數偏移量 * **num_columns_without_result**:函式計算結果的寫入列 * **input_rows_count**: block之中的資料行數 這裡本質上是呼叫了介面**IFunction**的介面,它的子類需要實現對應的`excuteImpl`的方法: ``` class IFunction : public std::enable_shared_f