ClickHouse原始碼筆記4:FilterBlockInputStream, 探尋where,having的實現
阿新 • • 發佈:2021-03-01
>書接上文,本篇繼續分享ClickHouse原始碼中一個重要的流,`FilterBlockInputStream`的實現,重點在於分析Clickhouse是如何在執行引擎實現向量化的Filter操作符,而利用這個Filter操作符的,就可以實現`where, having`的資料過濾。
話不多說,準備發車~~ 本文的原始碼分析基於ClickHouse v19.16.2.2的版本。
### 1.Selection的實現
`Selection`是關係代數之中重要的一個的一個運算,通常也會用`σ`符合來selection的實現。
而在SQL語句之中,實現Selection運算的便是:`where`與`having`。而本文就要從一個簡單的SQL語句出發,帶領大家一同梳理Clickhouse的原始碼,來探究它是如何實現選擇運算的。
先看如下的查詢
```SELECT * FROM test where a > 3 and b < 1;```
這裡掃描了`test`表,並且需要篩選出了`a`列大於3且`b`列小於1的行。老規矩,咱們先嚐試開啟ClickHouse的Debug日誌看一下具體的執行的**pipeline**。(ClickHouse 20.6之後的版本,終於支援了使用**Explain語句**來檢視執行計劃,真是千呼萬喚始出來啊~~)
![ClickHouse執行的Pipeline](https://upload-images.jianshu.io/upload_images/8552201-c4a2d3870f0ffe2d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
這裡分為了4個流,而咱們需要關注的流就是`Filter`流,它實現了從儲存引擎的資料讀取資料,並且執行函式運算,並最終實現資料過濾的邏輯。
所以Clickhouse的表示式計算並不單單隻由`ExpressionBlockInputStream`來完成的,而`FilterBlockInputStream`同樣也需要包含`Expression`進行表示式的向量化的計算與過濾。
**吐槽時間**:**私以為這樣的實現並不優雅,如果在`Filter`上層再套一層`ExpressionBlockinputStream`結構上會更加清晰。**不過這樣的實現可能會導致額外的效能損耗,Clickhouse為了實現查詢的高效執行可謂是『喪心病狂』, 後續分析聚合函式的實現時,我們會見到更為`Dirty`的程式碼。
### 2. FilterBlockInputStream的原始碼剖析
* **FilterBlockInputStream readImpl()的實現**
直接上程式碼看一下`FilterBlockInputStream`的資料讀取方法吧,這部分程式碼比較多。我們拆解出來梳理
```
/// Determine position of filter column.
header = input->getHeader();
expression->execute(header);
filter_column = header.getPositionByName(filter_column_name);
auto & column_elem = header.safeGetByPosition(filter_column);
/// Isn't the filter already constant?
if (column_elem.column)
constant_filter_description = ConstantFilterDescription(*column_elem.column);
```
首先,構造`FilterBlockInputStream`時會首先讀取下一級流的`Block Header`。通過`Header`來分析是否有常量列滿足`always true`或`always false`的邏輯,來設定`ConstantFilterDescription`。比如存在全部是`null`列的過濾列,無論進行什麼表示式計算,結果都是`false`。如果這樣的話,就直接放回空的`block`給上層流就ok了。
```
if (expression->checkColumnIsAlwaysFalse(filter_column_name))
return {};
// Function: checkColumnIsAlwaysFalse
for (auto & action : actions)
{
if (action.type == action.APPLY_FUNCTION && action.function_base)
{
auto name = action.function_base->getName();
if ((name == "in" || name == "globalIn")
&& action.result_name == column_name
&& action.argument_names.size() > 1)
{
set_to_check = action.argument_names[1];
}
}
}
```
接下來解析`FilterBlockInputStream`之中所有的表示式,查詢是否有`in`或`globalin`的函式呼叫,並且其第二個引數`set`為空,那麼同樣表示表示式`alwaysFalse`也可以直接返回為空的Block。
比如說有如下查詢:`select * from test2 where a in (select a from test2 where a > 10)`
而這個子查詢`select a from test2 where a > 10`返回的是空集的話,那麼就會被直接過濾了,返回空的block。
接下來進入一個`while`迴圈,不斷從底層的流讀取資料,並進行對應的表示式計算。這裡我刪去了一些冗餘的程式碼:
```
while (1)
{
res = children.back()->read();
expression->execute(res);
size_t columns = res.columns();
ColumnPtr column = res.safeGetByPosition(filter_column).column;
```
這裡的實現很簡單,就是不停從底層的流讀取資料Block,通過表示式計算生成`filter_column`列。這個列是一組`bool`列,標識了對應的行是否還應該存在。
舉個栗子,如果有如下查詢`select * from test where a > 10 and b < 2`。ClickHouse的表示式會生成如下執行流程如下(**注意:ClickHouse遵從函數語言程式設計的邏輯,任意函式呼叫都會生成新的一列**):
```
1. add const column : 10
2. function call : a > 10 (生成一組新生成的bool列,列名為`a > 10`)
3. remove const column : 10
4. add const column : 2
5. function call : b < 2 (生成一組新生成的bool列,列名為`b < 2`)
6. remove const column : 2
7. call function : a > 10 and b < 2 (生成一組新生成的bool列,列名為`a > 10 and b < 2`)
8. remove column : a > 10
9. remove column : b < 2
```
而最終新生成的這列就是我們後續需要用到過濾最終結果的`filter_column`列了。
接下來就進入最核心的一部分程式碼了,遍歷Block之中除了`const column`與`filter_column`列的所有列,進行實際的資料過濾。`IColumn`介面中實現了一個介面為`filter`,也就是說,每一個列型別都需要實現一個過濾方法,用一組bool陣列來過濾列資料。
```
/** Removes elements that don't match the filter.
* Is used in WHERE and HAVING operations.
* If result_size_hint > 0, then makes advance reserve(result_size_hint) for the result column;
* if 0, then don't makes reserve(),
* otherwise (i.e. < 0), makes reserve() using size of source column.
*/
using Filter = Padded