主頁 > 資料庫 > ClickHouse原始碼筆記4:FilterBlockInputStream, 探尋where,having的實作

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

2021-03-02 07:33:11 資料庫

書接上文,本篇繼續分享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陳述句來查看執行計劃,真是千呼萬喚始出來啊~~)

ClickHouse執行的Pipeline

這里分為了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源代碼

轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/265096.html

標籤:其他

上一篇:理解「分布式系統」曾經發生的事情

下一篇:Redis-第十章節-鏈表

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • GPU虛擬機創建時間深度優化

    **?桔妹導讀:**GPU虛擬機實體創建速度慢是公有云面臨的普遍問題,由于通常情況下創建虛擬機屬于低頻操作而未引起業界的重視,實際生產中還是存在對GPU實體創建時間有苛刻要求的業務場景。本文將介紹滴滴云在解決該問題時的思路、方法、并展示最終的優化成果。 從公有云服務商那里購買過虛擬主機的資深用戶,一 ......

    uj5u.com 2020-09-10 06:09:13 more
  • 可編程網卡芯片在滴滴云網路的應用實踐

    **?桔妹導讀:**隨著云規模不斷擴大以及業務層面對延遲、帶寬的要求越來越高,采用DPDK 加速網路報文處理的方式在橫向縱向擴展都出現了局限性。可編程芯片成為業界熱點。本文主要講述了可編程網卡芯片在滴滴云網路中的應用實踐,遇到的問題、帶來的收益以及開源社區貢獻。 #1. 資料中心面臨的問題 隨著滴滴 ......

    uj5u.com 2020-09-10 06:10:21 more
  • 滴滴資料通道服務演進之路

    **?桔妹導讀:**滴滴資料通道引擎承載著全公司的資料同步,為下游實時和離線場景提供了必不可少的源資料。隨著任務量的不斷增加,資料通道的整體架構也隨之發生改變。本文介紹了滴滴資料通道的發展歷程,遇到的問題以及今后的規劃。 #1. 背景 資料,對于任何一家互聯網公司來說都是非常重要的資產,公司的大資料 ......

    uj5u.com 2020-09-10 06:11:05 more
  • 滴滴AI Labs斬獲國際機器翻譯大賽中譯英方向世界第三

    **桔妹導讀:**深耕人工智能領域,致力于探索AI讓出行更美好的滴滴AI Labs再次斬獲國際大獎,這次獲獎的專案是什么呢?一起來看看詳細報道吧! 近日,由國際計算語言學協會ACL(The Association for Computational Linguistics)舉辦的世界最具影響力的機器 ......

    uj5u.com 2020-09-10 06:11:29 more
  • MPP (Massively Parallel Processing)大規模并行處理

    1、什么是mpp? MPP (Massively Parallel Processing),即大規模并行處理,在資料庫非共享集群中,每個節點都有獨立的磁盤存盤系統和記憶體系統,業務資料根據資料庫模型和應用特點劃分到各個節點上,每臺資料節點通過專用網路或者商業通用網路互相連接,彼此協同計算,作為整體提供 ......

    uj5u.com 2020-09-10 06:11:41 more
  • 滴滴資料倉庫指標體系建設實踐

    **桔妹導讀:**指標體系是什么?如何使用OSM模型和AARRR模型搭建指標體系?如何統一流程、規范化、工具化管理指標體系?本文會對建設的方法論結合滴滴資料指標體系建設實踐進行解答分析。 #1. 什么是指標體系 ##1.1 指標體系定義 指標體系是將零散單點的具有相互聯系的指標,系統化的組織起來,通 ......

    uj5u.com 2020-09-10 06:12:52 more
  • 單表千萬行資料庫 LIKE 搜索優化手記

    我們經常在資料庫中使用 LIKE 運算子來完成對資料的模糊搜索,LIKE 運算子用于在 WHERE 子句中搜索列中的指定模式。 如果需要查找客戶表中所有姓氏是“張”的資料,可以使用下面的 SQL 陳述句: SELECT * FROM Customer WHERE Name LIKE '張%' 如果需要 ......

    uj5u.com 2020-09-10 06:13:25 more
  • 滴滴Ceph分布式存盤系統優化之鎖優化

    **桔妹導讀:**Ceph是國際知名的開源分布式存盤系統,在工業界和學術界都有著重要的影響。Ceph的架構和演算法設計發表在國際系統領域頂級會議OSDI、SOSP、SC等上。Ceph社區得到Red Hat、SUSE、Intel等大公司的大力支持。Ceph是國際云計算領域應用最廣泛的開源分布式存盤系統, ......

    uj5u.com 2020-09-10 06:14:51 more
  • es~通過ElasticsearchTemplate進行聚合~嵌套聚合

    之前寫過《es~通過ElasticsearchTemplate進行聚合操作》的文章,這一次主要寫一個嵌套的聚合,例如先對sex集合,再對desc聚合,最后再對age求和,共三層嵌套。 Aggregations的部分特性類似于SQL語言中的group by,avg,sum等函式,Aggregation ......

    uj5u.com 2020-09-10 06:14:59 more
  • 爬蟲日志監控 -- Elastc Stack(ELK)部署

    傻瓜式部署,只需替換IP與用戶 導讀: 現ELK四大組件分別為:Elasticsearch(核心)、logstash(處理)、filebeat(采集)、kibana(可視化) 下載均在https://www.elastic.co/cn/downloads/下tar包,各組件版本最好一致,配合fdm會 ......

    uj5u.com 2020-09-10 06:15:05 more
最新发布
  • day02-2-商鋪查詢快取

    功能02-商鋪查詢快取 3.商鋪詳情快取查詢 3.1什么是快取? 快取就是資料交換的緩沖區(稱作Cache),是存盤資料的臨時地方,一般讀寫性能較高。 快取的作用: 降低后端負載 提高讀寫效率,降低回應時間 快取的成本: 資料一致性成本 代碼維護成本 運維成本 3.2需求說明 如下,當我們點擊商店詳 ......

    uj5u.com 2023-04-20 08:33:24 more
  • MySQL中binlog備份腳本分享

    關于MySQL的二進制日志(binlog),我們都知道二進制日志(binlog)非常重要,尤其當你需要point to point災難恢復的時侯,所以我們要對其進行備份。關于二進制日志(binlog)的備份,可以基于flush logs方式先切換binlog,然后拷貝&壓縮到到遠程服務器或本地服務器 ......

    uj5u.com 2023-04-20 08:28:06 more
  • day02-短信登錄

    功能實作02 2.功能01-短信登錄 2.1基于Session實作登錄 2.1.1思路分析 2.1.2代碼實作 2.1.2.1發送短信驗證碼 發送短信驗證碼: 發送驗證碼的介面為:http://127.0.0.1:8080/api/user/code?phone=xxxxx<手機號> 請求方式:PO ......

    uj5u.com 2023-04-20 08:27:27 more
  • 快取與資料庫雙寫一致性幾種策略分析

    本文將對幾種快取與資料庫保證資料一致性的使用方式進行分析。為保證高并發性能,以下分析場景不考慮執行的原子性及加鎖等強一致性要求的場景,僅追求最終一致性。 ......

    uj5u.com 2023-04-20 08:26:48 more
  • sql陳述句優化

    問題查找及措施 問題查找 需要找到具體的代碼,對其進行一對一優化,而非一直把關注點放在服務器和sql平臺 降低簡化每個事務中處理的問題,盡量不要讓一個事務拖太長的時間 例如檔案上傳時,應將檔案上傳這一步放在事務外面 微軟建議 4.啟動sql定時執行計劃 怎么啟動sqlserver代理服務-百度經驗 ......

    uj5u.com 2023-04-20 08:26:35 more
  • 云時代,MySQL到ClickHouse資料同步產品對比推薦

    ClickHouse 在執行分析查詢時的速度優勢很好的彌補了MySQL的不足,但是對于很多開發者和DBA來說,如何將MySQL穩定、高效、簡單的同步到 ClickHouse 卻很困難。本文對比了 NineData、MaterializeMySQL(ClickHouse自帶)、Bifrost 三款產品... ......

    uj5u.com 2023-04-20 08:26:29 more
  • sql陳述句優化

    問題查找及措施 問題查找 需要找到具體的代碼,對其進行一對一優化,而非一直把關注點放在服務器和sql平臺 降低簡化每個事務中處理的問題,盡量不要讓一個事務拖太長的時間 例如檔案上傳時,應將檔案上傳這一步放在事務外面 微軟建議 4.啟動sql定時執行計劃 怎么啟動sqlserver代理服務-百度經驗 ......

    uj5u.com 2023-04-20 08:25:13 more
  • Redis 報”OutOfDirectMemoryError“(堆外記憶體溢位)

    Redis 報錯“OutOfDirectMemoryError(堆外記憶體溢位) ”問題如下: 一、報錯資訊: 使用 Redis 的業務介面 ,產生 OutOfDirectMemoryError(堆外記憶體溢位),如圖: 格式化后的報錯資訊: { "timestamp": "2023-04-17 22: ......

    uj5u.com 2023-04-20 08:24:54 more
  • day02-2-商鋪查詢快取

    功能02-商鋪查詢快取 3.商鋪詳情快取查詢 3.1什么是快取? 快取就是資料交換的緩沖區(稱作Cache),是存盤資料的臨時地方,一般讀寫性能較高。 快取的作用: 降低后端負載 提高讀寫效率,降低回應時間 快取的成本: 資料一致性成本 代碼維護成本 運維成本 3.2需求說明 如下,當我們點擊商店詳 ......

    uj5u.com 2023-04-20 08:24:03 more
  • day02-短信登錄

    功能實作02 2.功能01-短信登錄 2.1基于Session實作登錄 2.1.1思路分析 2.1.2代碼實作 2.1.2.1發送短信驗證碼 發送短信驗證碼: 發送驗證碼的介面為:http://127.0.0.1:8080/api/user/code?phone=xxxxx<手機號> 請求方式:PO ......

    uj5u.com 2023-04-20 08:23:11 more