1. 程式人生 > >ClickHouse原始碼筆記4:FilterBlockInputStream, 探尋where,having的實現

ClickHouse原始碼筆記4:FilterBlockInputStream, 探尋where,having的實現

>書接上文,本篇繼續分享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