书接上文,本篇继续分享ClickHouse源码中一个重要的流,FilterBlockInputStream的实现,重点在于分析Clickhouse是如何在执行引擎实现向量化的Filter操作符,而利用这个Filter操作符的,就可以实现where, having的数据过滤。

话不多说,准备发车~~ 本文的源码分析基于ClickHouse v19.16.2.2的版本。

1.Selection的实现

Selection是关系代数之中重要的一个的一个运算,通常也会用σ符合来selection的实现。

而在SQL语句之中,实现Selection运算的便是:wherehaving。而本文就要从一个简单的SQL语句出发,带领大家一同梳理Clickhouse的源码,来探究它是如何实现选择运算的。

先看如下的查询

SELECT * FROM test where a > 3 and b < 1;

这里扫描了test表,并且需要筛选出了a列大于3且b列小于1的行。老规矩,咱们先尝试打开ClickHouse的Debug日志看一下具体的执行的pipeline。(ClickHouse 20.6之后的版本,终于支持了使用Explain语句来查看执行计划,真是千呼万唤始出来啊~~)

这里分为了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 truealways 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之中所有的表达式,查询是否有inglobalin的函数调用,并且其第二个参数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 columnfilter_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 = PaddedPODArray<UInt8>;
virtual Ptr filter(const Filter & filt, ssize_t result_size_hint) const = 0;

我们直接跳到子类的实现中来看一下:

template <typename T>
ColumnPtr ColumnVector<T>::filter(const IColumn::Filter & filt, ssize_t result_size_hint) const
{
const UInt8 * filt_pos = filt.data();
const UInt8 * filt_end = filt_pos + size;
const T * data_pos = data.data(); while (filt_pos < filt_end)
{
if (*filt_pos)
res_data.push_back(*data_pos); ++filt_pos;
++data_pos;
} return res;
}

这之中最为核心的就是这个while循环,遍历bool数组,然后将合法数据塞进一个新的列之中,最终新的列替换旧的列,就完成了一列数据的过滤。之后对于剩余的列依次按照上述流程过一遍就完成了整个block的过滤。这里也可以看到,这个while循环也是一组很简单,没有control flow break的一段代码,能够给予编译器向量化优化的空间很大。当然,ClickHouse还提供了一个手工调用向量化API的过滤版本代码:

#ifdef __SSE2__
/** A slightly more optimized version.
* Based on the assumption that often pieces of consecutive values
* completely pass or do not pass the filter.
* Therefore, we will optimistically check the parts of `SIMD_BYTES` values.
*/ static constexpr size_t SIMD_BYTES = 16;
const __m128i zero16 = _mm_setzero_si128();
const UInt8 * filt_end_sse = filt_pos + size / SIMD_BYTES * SIMD_BYTES; while (filt_pos < filt_end_sse)
{
int mask = _mm_movemask_epi8(_mm_cmpgt_epi8(_mm_loadu_si128(reinterpret_cast<const __m128i *>(filt_pos)), zero16)); if (0 == mask)
{
/// Nothing is inserted.
}
else if (0xFFFF == mask)
{
res_data.insert(data_pos, data_pos + SIMD_BYTES);
}
else
{
for (size_t i = 0; i < SIMD_BYTES; ++i)
if (filt_pos[i])
res_data.push_back(data_pos[i]);
} filt_pos += SIMD_BYTES;
data_pos += SIMD_BYTES;
}
#endif

熟悉向量化操作的同学应该会觉得上面的代码很简单。每一操作了16个bool数组,只是笔者比较好奇的是,原则上用avx2或者是avx512应该能够写出一批次处理更多数据的向量化代码,为啥没做呢?所以可能某些支持了指令集avx2avx512指令集上编译的上述代码,不打开SSE2的编译宏定义,通过编译器来实现自动向量化,可能执行速度会更快。

到这里,基本上完成了整个数据的过滤流程。当然,事情还没结束,我们的filter_column列通常是不需要再保留的,所以最后ClickHouse还会通过一段代码剔除这个列:

Block FilterBlockInputStream::removeFilterIfNeed(Block && block)
{
if (block && remove_filter)
block.erase(static_cast<size_t>(filter_column)); return std::move(block);
}

3.要点梳理

第二小节梳理完成了一整个filter数据流程的代码梳理,这里重点梳理一下实现向量化执行的要点:

  1. ClickHouse的计算是纯粹函数式编程式的计算,不会改变原先的列状态,而是产生一组新的列。我们需要使用这部分新生成bool列来进一步过滤数据。
  2. 通过对最终统一对bool列的处理,不仅保证了Cache的亲和度,同时也保证了代码的简洁,给自动化向量化优化提供了尽可能多的空间。最后手工编写了可以用编译宏打开的向量化代码,进一步保证了高效的向量化实现。
  3. 短路执行 vs 向量化执行 这部分ClickHouse的表达式过滤的执行逻辑与Doris完全不同。由于Clickhouse需要向量查询执行,所以函数表达式无法短路执行。 比如,如果你写 WHERE a > 1 AND b < 2,从上面的分析来看,ClickHouse都两个表达式都会进行计算,即使是所有的列都不满足a > 1但是b < 2也会进行计算。而当前Doris目前在存储引擎的列存过滤的表达式没有实现向量化过滤,而是通过短路过滤的形式。这两种方式目前看起来并没有绝对的优劣:显然,短路执行 vs 向量化执行的表现,很大程度上性能的表现取决于表达式的过滤率。

4. 小结

好了,到这里也就把ClickHouse函数数据过滤的代码梳理完了。

所以看到这里,大家相比对于ClickHouse之中如何高效的实现where, having有新的理解。

笔者是一个ClickHouse的初学者,对ClickHouse有兴趣的同学,欢迎多多指教,交流。

5. 参考资料

官方文档

ClickHouse源代码

ClickHouse源码笔记4:FilterBlockInputStream, 探寻where,having的实现的更多相关文章

  1. ClickHouse源码笔记5:聚合函数的源码再梳理

    笔者在源码笔记1之中分析过ClickHouse的聚合函数的实现,但是对于各个接口函数的实际如何共同工作的源码,回头看并没有那么明晰,主要原因是没有结合Aggregator的类来一起分析聚合函数的是如果 ...

  2. ClickHouse源码笔记3:函数调用的向量化实现

    分享一下笔者研读ClickHouse源码时分析函数调用的实现,重点在于分析Clickhouse查询层实现的接口,以及Clickhouse是如何利用这些接口更好的实现向量化的.本文的源码分析基于Clic ...

  3. ClickHouse源码笔记1:聚合函数的实现

    由于工作的需求,后续笔者工作需要和开源的OLAP数据库ClickHouse打交道.ClickHouse是Yandex在2016年6月15日开源了一个分析型数据库,以强悍的单机处理能力被称道. 笔者在实 ...

  4. ClickHouse源码笔记2:聚合流程的实现

    上篇笔记讲到了聚合函数的实现并且带大家看了聚合函数是如何注册到ClickHouse之中的并被调用使用的.这篇笔记,笔者会续上上篇的内容,将剖析一把ClickHouse聚合流程的整体实现. 第二篇文章, ...

  5. ClickHouse源码笔记6:探究列式存储系统的排序

    分析完成了聚合以及向量化过滤,向量化的函数计算之后.本篇,笔者将分析数据库的一个重要算子:排序.让我们从源码的角度来剖析ClickHouse作为列式存储系统是如何实现排序的. 本系列文章的源码分析基于 ...

  6. clickhouse源码Redhat系列机单机版安装踩坑笔记

    前情概要 由于工作需要用到clickhouse, 这里暂不介绍概念,应用场景,谷歌,百度一大把. 将安装过程踩下的坑记录下来备用 ClickHouse源码 git clone安装(直接下载源码包安装失 ...

  7. Zepto源码笔记(一)

    最近在研究Zepto的源码,这是第一篇分析,欢迎大家继续关注,第一次写源码笔记,希望大家多指点指点,第一篇文章由于首次分析原因不会有太多干货,希望后面的文章能成为各位大大心目中的干货. Zepto是一 ...

  8. redis源码笔记(一) —— 从redis的启动到command的分发

    本作品采用知识共享署名 4.0 国际许可协议进行许可.转载联系作者并保留声明头部与原文链接https://luzeshu.com/blog/redis1 本博客同步在http://www.cnblog ...

  9. AsyncTask源码笔记

    AsyncTask源码笔记 AsyncTask在注释中建议只用来做短时间的异步操作,也就是只有几秒的操作:如果是长时间的操作,建议还是使用java.util.concurrent包中的工具类,例如Ex ...

随机推荐

  1. E - Period(KMP中next数组的运用)

    一个带有 n 个字符的字符串 s ,要求找出 s 的前缀中具有循环结构的字符子串,也就是要输出具有循环结构的前缀的最后一个数下标与其对应最大循环次数.(次数要求至少为2) For each prefi ...

  2. Codeforces Round #622 (Div. 2) B. Different Rules(数学)

    Codeforces Round #622 (Div. 2) B. Different Rules 题意: 你在参加一个比赛,最终按两场分赛的排名之和排名,每场分赛中不存在名次并列,给出参赛人数 n ...

  3. windows10上下载远程控制软件之后导致windows10亮度调节功能不能使用

    出现了通用即插即用监视器就可以了

  4. Codeforces Round #631 div1C(或者div2E) Drazil Likes Heap 题解

    题目链接:https://codeforces.com/contest/1329/problem/C 或者:https://codeforces.com/contest/1330/problem/E ...

  5. Best Reward && Girls' research

    After an uphill battle, General Li won a great victory. Now the head of state decide to reward him w ...

  6. poj2362 Square

    Description Given a set of sticks of various lengths, is it possible to join them end-to-end to form ...

  7. 三、Jmeter 定时器

    首先需要清楚Jmeter中各个元件的执行顺序: 元件的执行顺序 了解了元件有作用域之后,来看看元件的执行顺序,元件执行顺序的规则很简单,在同一作用域名范围内,测试计划中的元件按照如下顺序执行. (1) ...

  8. CF1463-D. Pairs

    CF1463-D. Pairs 题意: 有从\(1\)到\(2n\)一共\(2n\)个数字,让你将这\(2n\)个数字分成\(n\)组,每组有两个数字.对于这\(n\)组数字,你可以从中挑选\(x\) ...

  9. 一篇文章图文并茂地带你轻松实践 HTML5 history api

    HTML5 history api 前言 由于笔者在网络上没有找到比较好的关于 history api 的实践案例,有的案例过于杂乱,没有重点,有些案例只是告诉读者 api 是什么,却没告诉怎么用,本 ...

  10. HDU 4280 Island Transport(HLPP板子)题解

    题意: 求最大流 思路: \(1e5\)条边,偷了一个超长的\(HLPP\)板子.复杂度\(n^2 \sqrt{m}\).但通常在随机情况下并没有isap快. 板子: template<clas ...