ClickHouse 架構概述之列(Columns)介紹
莫愁前路無知己,天下誰人不識君。
ClickHouse 是一個真正的列式數據庫管理系統(DBMS)。在 ClickHouse 中,數據始終是按列存儲的,包括矢量(向量或列塊)執行的過程。只要有可能,操作都是基於矢量進行分派的,而不是單個的值,這被稱為“矢量化查詢執行”,它有利於降低實際的數據處理開銷。
通常有兩種不同的加速查詢處理的方法:矢量化查詢執行和運行時代碼生成。在后者中,動態地為每一類查詢生成代碼,消除了間接分派和動態分派。這兩種方法中,並沒有哪一種嚴格地比另一種好。運行時代碼生成可以更好地將多個操作融合在一起,從而充分利用 CPU 執行單元和流水線。矢量化查詢執行不是特別實用,因為它涉及必須寫到緩存並讀回的臨時向量。如果 L2 緩存容納不下臨時數據,那么這將成為一個問題。但矢量化查詢執行更容易利用 CPU 的 SIMD 功能。ClickHouse 使用了矢量化查詢執行,同時初步提供了有限的運行時動態代碼生成。
列(Columns)
要表示內存中的列(實際上是列塊),需使用 IColumn
接口。該接口提供了用於實現各種關系操作符的輔助方法。幾乎所有的操作都是不可變的:這些操作不會更改原始列,但是會創建一個新的修改后的列。比如,IColumn::filter
方法接受過濾字節掩碼,用於 WHERE
和 HAVING
關系操作符中。另外的例子:IColumn::permute
方法支持 ORDER BY
實現,IColumn::cut
方法支持 LIMIT
實現等等。
IColumn::filter和IColumn::permute
方法作為純虛函數,代碼位於dbms\src\Columns目錄下的IColumn.h中,在子類中有具體的實現,比如ColumnDecimal子類。
IColumn::cut方法在基類IColumn中實現,用於LIMIT操作。
不同的 IColumn
實現(ColumnUInt8
、ColumnString
等)負責不同的列內存布局。內存布局通常是一個連續的數組。對於數據類型為整型的列,只是一個連續的數組,比如 std::vector
。對於 String
列和 Array
列,則由兩個向量組成:其中一個向量連續存儲所有的 String
或數組元素,另一個存儲每一個 String
或 Array
的起始元素在第一個向量中的偏移。而 ColumnConst
則僅在內存中存儲一個值,但是看起來像一個列。
對於ColumnString類,Chars用於存儲所有的String,offsets用於存儲每個String在Chars中的偏移。代碼位於dbms\src\Columns目錄下的ColumnString.h和ColumnString.cpp中。
對於ColumnArray類,data用於存儲數組元素,offsets用於存儲每個Array在data中的偏移,代碼位於dbms\src\Columns目錄下的ColumnArray.h和ColumnArray.cpp中。
Column相關的代碼目錄位於dbms\src\Columns路徑下,涉及到的類及相互關系如下圖所示:
IColumn
具有用於數據的常見關系轉換的方法,但這些方法並不能夠滿足所有需求。比如,ColumnUInt64
沒有用於計算兩列和的方法,ColumnString
沒有用於進行子串搜索的方法。這些無法計算的例程在 Icolumn
之外實現。
列(Columns)上的各種函數可以通過使用 Icolumn
的方法來提取 Field
值,或根據特定的 Icolumn
實現的數據內存布局的知識,以一種通用但不高效的方式實現。為此,函數將會轉換為特定的 IColumn
類型並直接處理內部表示。比如,ColumnUInt64
具有 getData
方法,該方法返回一個指向列的內部數組的引用,然后一個單獨的例程可以直接讀寫或填充該數組。實際上,“抽象漏洞(leaky abstractions)”允許我們以更高效的方式來實現各種特定的例程。