
ClickHouse是一個完全面向列式的分布式數(shù)據(jù)庫。數(shù)據(jù)通過列存儲,在查詢過程中,數(shù)據(jù)通過數(shù)組來處理(向量或者列Chunk)。當(dāng)進行查詢時,操作被轉(zhuǎn)發(fā)到數(shù)組上,而不是在特定的值上。因此被稱為”向量化查詢執(zhí)行”,相對于實際的數(shù)據(jù)處理成本,向量化處理具有更低的轉(zhuǎn)發(fā)成本。
這個設(shè)計思路并不是新的思路理念。歷史可以追溯到``APL``編程語言時代:``A+``, ``J``, ``K``, and ``Q``。數(shù)組編程廣泛用于科學(xué)數(shù)據(jù)處理領(lǐng)域。而在關(guān)系型數(shù)據(jù)庫中:也應(yīng)用了``向量化``系統(tǒng)。
在加速查詢處理上,有兩種的方法:向量化查詢執(zhí)行和運行時代碼生成。為每種查詢類型都進行代碼生成,去除所有的間接和動態(tài)轉(zhuǎn)發(fā)處理。這些方法并不比其他方法好,當(dāng)多個操作一起執(zhí)行時,運行時代碼生成會更好,可以充分累用CPU執(zhí)行單元和Pipeline管道。
向量化查詢執(zhí)行實用性并不那么高,因為它涉及到臨時向量,必須寫到緩存中,并讀取回來。如果臨時數(shù)據(jù)并不適合L2緩存,它可能是一個問題。但是向量化查詢執(zhí)行更容易利用CPU的SIMD能力。一個研究論文顯示將兩個方法結(jié)合到一起效果會更好。ClickHouse主要使用向量化查詢執(zhí)行和有限的運行時代碼生成支持(僅GROUP BY內(nèi)部循環(huán)第一階段被編譯)。
列
-------
為了表示內(nèi)存中的列(列的 chunks),``IColumn``將被使用。這個接口提供了一些輔助方法來實現(xiàn)不同的關(guān)系操作符。幾乎所有的操作符都是非更改的:他們不能更改原有的列,但是創(chuàng)建一個新的更新的列。例如,IColumn::filter方法接受一個過濾器字節(jié)掩碼,同時創(chuàng)建一個新的過濾列。它被用在WHERE和HAVING的關(guān)系操作符上。額外的示例:IColumn::permute方法支持ORDER BY,IColumn::cut方法支持LIMIT等。
不同的IColumn實現(xiàn)(ColumnUInt8,ColumnString等)負(fù)責(zé)列的內(nèi)存布局。內(nèi)存布局通常是一個連續(xù)的數(shù)組。對于列的整型來說,它是一個連續(xù)的數(shù)組,如std::vector。對于String和Array列,這個是2個vectors:一個是所有的數(shù)組元素,連續(xù)放置,另一個是偏移量(offsets),位于每個數(shù)組的起始端。也有ColumnConst用于在內(nèi)存中存儲一個值,但是它看起來像一個列。
數(shù)據(jù)域
-------------
然而, 它也可能工作在單獨的值上面。為了表示一個單獨的值。數(shù)據(jù)域使用.Fieldis 這是一個UInt64,Int64,Float64,StringandArray可區(qū)分的集合。IColumn 有operator[]方法來獲得n-th值作為一個數(shù)據(jù)域,insert[] 方法追加一個數(shù)據(jù)域到一個列的末尾。這些方法不是特別高效,因為他們需要處理臨時的數(shù)據(jù)域?qū)ο?,它代表一個單獨的值。這是一個最高效的方法,例如insertFrom, insertRangeFrom等。
對于一個表,一個特定的數(shù)據(jù)類型,數(shù)據(jù)域沒有足夠的信息。例如 ,UInt8,UInt16,UInt32, 和 UInt64都用 UInt64表示。
抽象滲漏法則
------------
IColumn有方法用于通用的關(guān)系型數(shù)據(jù)轉(zhuǎn)換,但是它并不能滿足所有需求。例如,ColumnUInt64沒有方法來計算2個列的加和,ColumnString沒有方法用于運行子字符串的搜索。一些進程是在IColumn之外實現(xiàn)的。
列中的不同函數(shù)能夠以一個通用的方式來實現(xiàn),使用IColumn方法來抽取數(shù)據(jù)域值,或者在特定的方法下使用數(shù)據(jù)的內(nèi)部內(nèi)存布局在特定的IColumn上實現(xiàn)。為了完成這個,函數(shù)將被轉(zhuǎn)換成一個特定的IColumn類型,直接在內(nèi)部進行處理。例如,ColumnUInt64有一個getData方法,將返回一個內(nèi)存數(shù)組的引用,然后一個單獨的進程讀取或者直接填充這個數(shù)組。事實上,我們有一個抽象滲漏法則來允許不同進程的專用化。
數(shù)據(jù)類型
-----------
IDataType 負(fù)責(zé)序列化和反序列化: 讀寫這個列的值或者以二進制或文本的方式的值.IDataType 直接與表中的數(shù)據(jù)類型一致。例如,有DataTypeUInt32,DataTypeDateTime,DataTypeString等。
IDataType和IColumnare 互相是松耦合的。不同的數(shù)據(jù)類型能夠在內(nèi)存中表示,通過相同的IColumn 實現(xiàn).。例如,DataTypeUInt32和DataTypeDateTime都是通過ColumnUInt32或者ColumnConstUInt32來表示。另外,相同的數(shù)據(jù)類型通過不同的IColumn實現(xiàn)來表示. 例如,DataTypeUInt8 能夠通過ColumnUInt8或者ColumnConstUInt8.來表示。
IDataType 僅存儲元數(shù)據(jù)。例如,DataTypeUInt8 根本不保存任何數(shù)據(jù) (除了 vptr) ,同時DataTypeFixedString 保存justN(確定的字符串大小)。
IDataType 對于不同的數(shù)據(jù)格式都有協(xié)助方法。示例是有些方法可以序列化一個值, 序列化一個值到 JSON,序列化一個值到 XML 格式。沒有直接的數(shù)據(jù)格式一一對應(yīng)。例如,不同的數(shù)據(jù)格式Pretty和TabSeparated 能夠使用相同的serializeTextEscaped協(xié)助方法,在IDataType接口中。
數(shù)據(jù)塊
-----------
一個數(shù)據(jù)塊是一個容器,代表了內(nèi)存中一個表的子集。它也是三元組的集合:(IColumn,IDataType,columnname). 在查詢執(zhí)行過程中, 數(shù)據(jù)通過數(shù)據(jù)塊來處理. 如果你有一個數(shù)據(jù)塊, 我們有數(shù)據(jù)(在IColumn對象中), 我們有這個數(shù)據(jù)的類型(在IDataType中) 告訴我們怎樣處理此列,同時我們有此列名稱 (或者是原有列名, 或者是人工命名,得到計算的臨時結(jié)果)。
在一個數(shù)據(jù)塊中,當(dāng)我們計算跨列某個函數(shù)時, 我們添加另外的帶有結(jié)果的列到數(shù)據(jù)塊中, 我們并不修改這個列,因為這些操作都是非變更的。然后,不需要的列將從數(shù)據(jù)塊中刪除,但不是修改。這個對于消除子表達式是便捷的。
數(shù)據(jù)塊為了每個處理的數(shù)據(jù) Chunk 創(chuàng)建的。 對于相同的計算類型,列名稱和類型對于不同的數(shù)據(jù)塊將保持一致, 只有列數(shù)據(jù)保持變化。這樣有利于更好地從數(shù)據(jù)塊頭拆分?jǐn)?shù)據(jù),因為小的數(shù)據(jù)塊大小將有高的臨時字符串開銷,當(dāng)拷貝 shared_ptrs 和 column names時。
數(shù)據(jù)塊流
-----------
數(shù)據(jù)塊流用于處理數(shù)據(jù)。我們使用數(shù)據(jù)塊的數(shù)據(jù)流從某處讀取數(shù)據(jù),執(zhí)行數(shù)據(jù)轉(zhuǎn)換或者寫入數(shù)據(jù)到某處。IBlockInputStream 有一個read方法獲取下一個數(shù)據(jù)塊。IBlockOutputStream 有一個write方法發(fā)送數(shù)據(jù)塊到某處。
數(shù)據(jù)流負(fù)責(zé):
讀寫一個表。當(dāng)讀寫數(shù)據(jù)塊時,此表將返回一個數(shù)據(jù)流。
實現(xiàn)數(shù)據(jù)格式。例如,如果你想要輸出數(shù)據(jù)以Pretty的格式到一個終端時。你將創(chuàng)建一個數(shù)據(jù)塊輸出流,然后格式化這個數(shù)據(jù)塊。
執(zhí)行數(shù)據(jù)轉(zhuǎn)換。你有BlockInputStream 同時 想要創(chuàng)建一個過濾數(shù)據(jù)流。你創(chuàng)建FilterBlockInputStream,初始化它。然后當(dāng)你從 FilterBlockInputStream拉取一個數(shù)據(jù)塊時,它將從數(shù)據(jù)流中獲得到一個數(shù)據(jù)塊,,過濾它,然后返回已經(jīng)過濾的數(shù)據(jù)塊給你。查詢執(zhí)行的 Pipeline 將展示這個方式。
有一些更加綜合的轉(zhuǎn)換。例如,當(dāng)你從AggregatingBlockInputStream拉取數(shù)據(jù)時,它將從數(shù)據(jù)源上讀取所有的數(shù)據(jù),聚合它,然后為你返回一個匯總數(shù)據(jù)流。另一個示例:UnionBlockInputStream接收很多輸入數(shù)據(jù)源和一些線程。它啟動了多個線程,從多個數(shù)據(jù)源中并行讀取數(shù)據(jù)。
數(shù)據(jù)塊流使用“pull” 的方式來控制數(shù)據(jù)流:當(dāng)你從第一個數(shù)據(jù)流中拉取一個數(shù)據(jù)塊時,它從嵌套的數(shù)據(jù)流中拉取所需要的數(shù)據(jù)塊,整個執(zhí)行 pipeline 將正常工作。其實“pull” 和 “push”都不是最佳方案,因為流控是隱式的,限制了不同特性的實現(xiàn),如多個查詢的并行執(zhí)行(一起合并多個 pipeline)。此限制是協(xié)程或者運行互相等待的外部線程。我們也能夠更多的可能性,如果我們進行顯式的流控:如果我們定位這個邏輯,從一個計算單元傳遞數(shù)據(jù)到外部的一個計算單元。更多的想法,參考此文章。
查詢執(zhí)行流水線將在每個步驟創(chuàng)建臨時數(shù)據(jù)。我們將保持?jǐn)?shù)據(jù)塊大小要足夠小,因此臨時數(shù)據(jù)要適合CPU緩存。假設(shè),讀寫臨時數(shù)據(jù)幾乎是自由的,相對于其他計算來說。我們可以考慮一個替代方案,融合多個操作在 pipeline 中,讓 pipeline 盡可能小,刪除盡可能多的臨時數(shù)據(jù)。這個可以是一個優(yōu)勢,也可能是個劣勢。例如,一個拆分 pipeline 將容易實現(xiàn)緩存中間數(shù)據(jù),從類似的查詢中偷取中間數(shù)據(jù),然后對于類似查詢,合并 pipeline。
格式
--------
數(shù)據(jù)格式用數(shù)據(jù)塊流來實現(xiàn)。有種“顯示性”格式僅適用于數(shù)據(jù)輸出到客戶端,例如 Pretty 格式, 它僅提供 IBlockOutputStream。有輸入輸出格式,例如 TabSeparated 或JSONEachRow。
也有行數(shù)據(jù)流: IRowInputStream和 IRowOutputStream. 他們允許你按照行來推/拉數(shù)據(jù), 而不是通過數(shù)據(jù)塊. 他們僅被用于簡化面向行格式的實現(xiàn)。封裝器BlockInputStreamFromRowInputStream 和BlockOutputStreamFromRowOutputStream 允許你轉(zhuǎn)換面向行的數(shù)據(jù)流到面向數(shù)據(jù)塊的數(shù)據(jù)流。
I/O
------
對于面向字節(jié)的輸入/輸出。有 ReadBuffer 和 WriteBuffer 抽象類. 他們被用于替代C++ iostream。 不用擔(dān)心:每個成熟的 C++ 工程都用更優(yōu)的類庫.
ReadBuffer 和 WriteBuffer 是一個連續(xù)的Buffer,游標(biāo)指向Buffer的位置. 具體實現(xiàn)可能有或沒有內(nèi)存。有個虛方法來用如下數(shù)據(jù)填充Buffer填充。 (對于ReadBuffer) 或者刷新Buffer到某處 (對于 WriteBuffer). 虛方法很少被調(diào)用。
Implementations of ReadBuffer/WriteBuffer 的實現(xiàn)被用于文件,文件描述和網(wǎng)絡(luò)套接字的處理,如實現(xiàn)壓縮 (CompressedWriteBuffer 用另外的 WriteBuffer 來初始化,在寫數(shù)據(jù)之前執(zhí)行壓縮),或者用于其他目的? – 名稱ConcatReadBuffer, LimitReadBuffer, 和HashingWriteBuffer 等。
Read/WriteBuffers 僅用于處理字節(jié),帶有格式化的輸入/輸出 (例如, 以decimal的方式寫入一個數(shù)字), 有一些函數(shù)是來自ReadHelpers 和WriteHelpers 頭文件的。
讓我們看一下當(dāng)你想以Json的格式寫入結(jié)果集到標(biāo)準(zhǔn)輸出時發(fā)生了什么。你有一個結(jié)果集準(zhǔn)備從IBlockInputStream獲取。你創(chuàng)建了 WriteBufferFromFileDescriptor(STDOUT_FILENO) 寫入字節(jié)到標(biāo)準(zhǔn)輸出. 你創(chuàng)建JSONRowOutputStream, 用 WriteBuffer來初始化, 寫入行到標(biāo)準(zhǔn)輸出。你在行輸出流之上創(chuàng)建 數(shù)據(jù)塊輸出流BlockOutputStreamFromRowOutputStream, 用IBlockOutputStream顯示它. 然后調(diào)用 copyData從IBlockInputStream 到 IBlockOutputStream來傳輸數(shù)據(jù). 從內(nèi)部來看, JSONRowOutputStream 將寫入不同的 JSON分隔符,調(diào)用 IDataType::serializeTextJSON 方法 引用到IColumn ,同時行數(shù)作為參數(shù)。然后,IDataType::serializeTextJSON將從 WriteHelpers.h調(diào)用一個方法:例如, 對于數(shù)字類型用writeText, 對于字符串類型用writeJSONString。
表
------
表通過IStorage接口來表示.對此接口不同的實現(xiàn)成為不同的表引擎. 例如 StorageMergeTree, StorageMemory, 等,這些類的實例是表。
最重要的IStorage 方法是讀和寫操作. 也有alter, rename, drop, 等操作. 讀方法接受如下的參數(shù):從表中讀取的列集合,? AST 查詢, 返回需要的數(shù)據(jù)流的數(shù)量. 它返回一個或多個 IBlockInputStream 對象和有關(guān)數(shù)據(jù)處理階段的信息,在查詢的過程中在表引擎中完成。
在大多數(shù)情況下,read方法負(fù)責(zé)從表中讀取特定的列,不進行后續(xù)的7數(shù)據(jù)處理。所有的進一步數(shù)據(jù)處理通過查詢中斷器來完成,這個在IStorage處理范圍之外。
但是也有一些例外: - AST 查詢被傳遞到read方法,表引擎使用它來衍生對索引的使用, 同時從一個表中讀取少量數(shù)據(jù). - 有時表引擎能夠處理數(shù)據(jù)到一個特定的階段。例如, StorageDistributed 能夠發(fā)送一個查詢到遠(yuǎn)程服務(wù)器,讓他們處理數(shù)據(jù)到一個階段,即來自不同遠(yuǎn)程服務(wù)器的數(shù)據(jù)能夠被合并,同時返回預(yù)處理后數(shù)據(jù) 查詢中斷器隨即結(jié)束對數(shù)據(jù)的處理。
表的read方法能夠返回多個IBlockInputStream 對象允許并行處理數(shù)據(jù). 這些多個數(shù)據(jù)塊輸入流能夠從一個表中并行讀取數(shù)據(jù). 然后你能夠用不同的轉(zhuǎn)換來封裝這些數(shù)據(jù)流(例如表達式評估,數(shù)據(jù)過濾) 能夠被單獨計算,同時在它們之上創(chuàng)建一個UnionBlockInputStream, 從多個數(shù)據(jù)流中并行讀取。
也有一些TableFunction. 有一些函數(shù)返回臨時的``IStorage 對象,用在查詢的 FROM 語句中.
為了快速建立一個印象,怎樣實現(xiàn)你自己的表引擎,如StorageMemory或StorageTinyLog。
作為read方法的結(jié)果, IStorage 返回 QueryProcessingStage – 此信息將返回哪個查詢部分已經(jīng)在Storage中被計算. 當(dāng)前,我們僅有非常粗粒度的信息。對于存儲來說,沒有方法說“我已經(jīng)處理了Where條件中的表達式部分,對于此數(shù)據(jù)范圍” 。我們需要工作在其上。
解析器
------------
一個查詢通過手寫的遞歸解析器被解析。例如, ParserSelectQuery遞歸調(diào)用如下的解析,對于不同的查詢部分。解析器創(chuàng)建了一個AST. 這個AST通過節(jié)點來表示,它是一個IAST實例。
由于歷史原因,解析器生成并沒有被使用。
中斷器
-----------
中斷器負(fù)責(zé)從一個AST上創(chuàng)建查詢執(zhí)行Pipeline。有一些簡單的中斷器,例如 InterpreterExistsQuery``和``InterpreterDropQuery, 或者更復(fù)雜一些的 InterpreterSelectQuery. 此查詢執(zhí)行pipeline是數(shù)據(jù)塊輸入個輸出流的結(jié)合體。例如,中斷SELECT 查詢的結(jié)果是IBlockInputStream 讀取結(jié)果集; INSERT 查詢的結(jié)果是 IBlockOutputStream 為了插入而寫入數(shù)據(jù);and the result of interpreting the中斷 INSERT SELECT 查詢的結(jié)果是在第一次讀取時,返回一個空結(jié)果集, 但是同時從SELECT到INSERT拷貝數(shù)據(jù)。
InterpreterSelectQuery 使用了ExpressionAnalyzer和ExpressionActions 機制來查詢分析和轉(zhuǎn)換。 這是一個基于規(guī)則的查詢優(yōu)。ExpressionAnalyzer 是有點亂的,應(yīng)該被重寫: 不同的查詢轉(zhuǎn)換和優(yōu)化應(yīng)該被提取到不同的類,來允許模塊化的轉(zhuǎn)化和查詢。
函數(shù)
----------
有一些普通函數(shù)和聚合函數(shù)。 對于聚合函數(shù),請查看下一個章節(jié)。
普通函數(shù)并不能改變行的數(shù)量 – 他們單獨處理每個行。事實上,對于每個行,函數(shù)不能被調(diào)用,但是對于數(shù)據(jù)塊的數(shù)據(jù)可實現(xiàn)向量化查詢執(zhí)行。
有一些 混合函數(shù), 例如blockSize, rowNumberInBlock, 和runningAccumulate, 拓展了數(shù)據(jù)塊處理,違反了行的獨立性。
ClickHouse 有強類型,因此隱式類型轉(zhuǎn)換不能執(zhí)行。如果函數(shù)不支持一個特定的類型綁定,異常將會拋出。但是函數(shù)能夠工作在很多不同的類型關(guān)聯(lián)。例如, plus 函數(shù) (實現(xiàn)了 + 操作符) 能夠工作在任意的數(shù)字類型關(guān)聯(lián):UInt8 + Float32, UInt16 + Int8, 等。一些變種函數(shù)能夠接收任意數(shù)量的參數(shù),如concat 函數(shù)。
聚合函數(shù)
---------------
聚合函數(shù)是狀態(tài)函數(shù)。 他們積累傳遞的值到某個狀態(tài), 允許你從這個狀態(tài)獲得結(jié)果。他們用IAggregateFunction來管理。狀態(tài)可以很簡單 (對于 AggregateFunctionCount 的狀態(tài)是一個單UInt64值) 或者相當(dāng)復(fù)雜 (AggregateFunctionUniqCombined 的狀態(tài)是與線性數(shù)組相關(guān), 一個哈希表, 一個 HyperLogLog 概率性數(shù)據(jù)結(jié)構(gòu))。
為了處理多個狀態(tài),當(dāng)執(zhí)行一個高基數(shù) GROUP BY 查詢, 狀態(tài)被分配在Arena中(一個內(nèi)存池), 或者他們能夠以任意合適的內(nèi)存分片被分配. 狀態(tài)可以有一個非細(xì)碎的構(gòu)造器和析構(gòu)器:例如, 復(fù)雜的聚合狀態(tài)能夠自己分配額外的內(nèi)存,這塊需要注意,對于創(chuàng)建和銷毀狀態(tài),同時傳遞他們的所屬關(guān)系,追蹤是誰和什么時候?qū)N毀這個狀態(tài)。
聚合狀態(tài)能夠序列化和反序列化來跨網(wǎng)絡(luò)傳遞,在執(zhí)行分布式查詢期間,或者如果沒有足夠的內(nèi)存情況下,將他們寫入到磁盤. 他們甚至能夠存儲到表內(nèi),DataTypeAggregateFunction 允許增量聚合數(shù)據(jù)。
對于聚合函數(shù)狀態(tài),序列化的數(shù)據(jù)格式目前不是版本化的。如果聚合狀態(tài)僅是臨時存儲,那是沒問題的。但是對于增量聚合,我們有AggregatingMergeTreetable 引擎,同時很多用戶已經(jīng)在生產(chǎn)環(huán)境中使用他們了。這就是為什么我們應(yīng)該增加向后兼容的支持,未來當(dāng)為任意的聚合函數(shù)更改序列化格式時。
服務(wù)器
-----------
服務(wù)器實現(xiàn)了不同的接口:
- 對于任意的外部客戶端暴露一個HTTP接口
- 對于本地客戶端暴露一個TCP 接口,在分布式查詢執(zhí)行時,用于跨服務(wù)器通信
- 一個接口用于傳輸同步數(shù)據(jù)
從內(nèi)部來講,這是一個基本的多線程服務(wù)器,沒有攜程, fibers, 等。服務(wù)器并沒有為高頻率短查詢來設(shè)計,而是為了處理低頻率的復(fù)雜查詢, 這兩種方式處理的數(shù)據(jù)量是不同的。
對于查詢執(zhí)行,服務(wù)器初始化上下文類,包括數(shù)據(jù)庫列表,用戶,訪問權(quán)限,設(shè)置,集群,處理列表,查詢?nèi)罩?,等。這個上下文環(huán)境被中斷器使用。
對于服務(wù)器的TCP協(xié)議,我們維護了向前兼容和向后兼容:老客戶端能訪問新服務(wù)器,新客戶端能訪問老服務(wù)器。但是我們不想一直維護它們, 未來一年我們將停止對老版本的支持。
對于外部應(yīng)用,我們推薦使用 HTTP 接口,因為它比較簡單易用。TCP 協(xié)議與內(nèi)部數(shù)據(jù)結(jié)構(gòu)有很多關(guān)聯(lián)耦合:它使用一個內(nèi)部結(jié)構(gòu)來傳遞數(shù)據(jù)塊,使用自定義的幀來用于壓縮。 對于此協(xié)議我們沒有發(fā)布一個 C 的庫,因為它需要連接大部分 ClickHouse 的代碼庫, 這么做不實際。
分布式查詢執(zhí)行
--------------------------
在一個集群設(shè)置中的服務(wù)器大部分是獨立的。你能夠在一個或所有的服務(wù)器上創(chuàng)建一個分布式表。此分布式表本身不存儲數(shù)據(jù)—在集群的多個節(jié)點上,僅提供一個"視圖"到所有的本地表。當(dāng)你從分布式表進行查詢時,它重寫這個查詢,根據(jù)負(fù)載均衡的設(shè)置,選擇遠(yuǎn)程節(jié)點,發(fā)送查詢給他們。
分布式表請求遠(yuǎn)程服務(wù)器來處理一個查詢到一個階段,此階段從不同的服務(wù)器中繼結(jié)果后進行合并。然后接收結(jié)果后合并這些結(jié)果。分布式表嘗試分布盡可能多的工作到遠(yuǎn)程服務(wù)器,不能跨網(wǎng)絡(luò)發(fā)送太多的中繼數(shù)據(jù)。
當(dāng)你進行 IN 或 JOIN 子查詢時,情況變得更加復(fù)雜一些,每個子查詢都使用一個分布式表。我們有不同的策略來執(zhí)行這些查詢。
對于分布式查詢執(zhí)行,沒有一個全局的查詢規(guī)劃。每個節(jié)點有自己的本地查詢規(guī)劃作為任務(wù)的一部分。我們僅有一個簡化的一步分布式查詢執(zhí)行:我們?yōu)檫h(yuǎn)程節(jié)點發(fā)送查詢,然后合并結(jié)果集。但是對于高基數(shù)的GROUP BY高難度查詢是并不可行的,或者大量臨時數(shù)據(jù)的 JOIN 查詢。ClickHouse并不支持這種查詢方式,我們需要進一步開發(fā)它。
合并樹
------------
合并樹(MergeTree)是存儲引擎的族,通過主鍵來支持索引. 主鍵可以是列或表達式的任意 tuple。在MergeTree表中的數(shù)據(jù)被存儲在 “parts” 中. 每一部分按照主鍵順序存儲數(shù)據(jù) (數(shù)據(jù)通過主鍵 tuple 來排序). 所有的表的列都在各自的column.bin文件中保存。 此文件由壓縮的數(shù)據(jù)塊組成。每個數(shù)據(jù)塊大小從64 KB 到 1 MB,依賴于平均值的大小。數(shù)據(jù)塊由列值組成,按順序連續(xù)放置。對于每一列,列值在同一個順序上 (順序通過主鍵來定義), 因此,對于對應(yīng)的列,當(dāng)你通過多列迭代以后來獲得值。
主鍵自身是"稀疏的"。它不定位到每個行 ,但是僅是一些數(shù)據(jù)范圍。 對于每個N-th行, 一個單獨的primary.idx 文件有主鍵的值, N 被稱為 index_granularity(通常情況下, N = 8192). 對于每個列, 我們有column.mrk 文件 ,帶有 “marks”標(biāo)簽,對于數(shù)據(jù)文件中的每個N-th行,它是一個偏移量 。每個標(biāo)簽都成成對兒出現(xiàn)的:文件中的偏移量到壓縮數(shù)據(jù)塊的起始端,解壓縮數(shù)據(jù)塊的偏移量到數(shù)據(jù)的起始端。 通常情況下,壓縮的數(shù)據(jù)塊通過"marks"標(biāo)簽來對齊,解壓縮的數(shù)據(jù)塊的偏移量是0。對于primary.idx的數(shù)據(jù)通常主流在存儲中,對于column.mrk文件的數(shù)據(jù)放在緩存中。
當(dāng)我們從MergeTree引擎中讀取數(shù)據(jù)時,我們看到了 primary.idx 數(shù)據(jù)和定位了可能包含請求數(shù)據(jù)的范圍, 然后進一步看column.mrk 數(shù)據(jù),和計算偏移量從哪開始讀取這些范圍。因為稀疏性, 超額的數(shù)據(jù)可能被讀取。 ClickHouse 并不適合高負(fù)載的點狀查詢,因為帶有索引粒度行的整個范圍必須被讀取, 整個壓縮數(shù)據(jù)塊必須被解壓縮。我們構(gòu)建的結(jié)構(gòu)是索引稀疏的,因為我們必須在單臺服務(wù)器上維護數(shù)萬億條數(shù)據(jù), 對于索引來說沒有顯著的內(nèi)存消耗。因為主鍵是稀疏的,它并不是唯一的:在 INSERT時,它不能夠檢查鍵的存在。在一個表內(nèi),相同的鍵你可以有多個行。
當(dāng)你插入大量數(shù)據(jù)進入MergeTree時,數(shù)據(jù)通過主鍵順序來篩選,形成一個新的部分。為了保持?jǐn)?shù)據(jù)塊數(shù)是低位的,有一些背景線程周期性地查詢這些數(shù)據(jù)塊,將他們合并到一個排序好的數(shù)據(jù)塊。
這就是為什么稱為MergeTree。當(dāng)然,合并意味著"寫入凈化"。所有的部分都是非修改的:他們僅創(chuàng)建和刪除,但是不會更新。當(dāng)SELECT運行時,它將獲得一個表的快照。在合并之后,我們也保持舊的部分用于故障數(shù)據(jù)恢復(fù),所以如果我們某些合并部分的文件損壞了,我們能夠根據(jù)原來的部分進行替換。
MergeTree 不是一個LSM 樹,因為它不包含? “memtable” 和 “l(fā)og”: 插入的數(shù)據(jù)直接寫入到文件系統(tǒng)。這個僅適合于批量的INSERT操作,并不是每行寫入,同時不能過于頻繁 – 每秒一次寫入是 OK 的,每秒幾千次寫入是不可以的。 我們使用這種方式是為了簡化,因為在生產(chǎn)環(huán)境中,我們主要以批量插入數(shù)據(jù)為主。
MergeTree表只有一個(主)索引:沒有二級索引。它允許在一個邏輯表下的多個物理表示,例如,在多個物理表中存儲數(shù)據(jù),甚至允許沿著原有的數(shù)據(jù)帶有預(yù)計算的表示。
有MergeTree引擎作為背景線程來做額外的合并。示例是CollapsingMergeTree和AggregatingMergeTree。他們作為對更新的特定支持來看待。這些并不是真的更新,在背景合并運行時,因為用戶沒法控制時間,在MergeTreetable中的數(shù)據(jù)經(jīng)常被存儲到多個部分,以非完全的合并形式。