【ClickHouse 內(nèi)核原理圖文詳解】關于分區(qū)、索引、標記和壓縮數(shù)據(jù)的協(xié)同工作

概述

ClickHouse 是一個用于聯(lián)機分析處理(OLAP)的列式數(shù)據(jù)庫管理系統(tǒng)(Columnar DBMS)。

分區(qū)、索引、標記和壓縮數(shù)據(jù),這些組件配合在一起給 ClickHouse 數(shù)據(jù)庫帶來非常高效的查詢性能。

一切皆是映射。光劍

本文先簡單介紹一下這幾個組件。然后就分別從寫入過程、查詢過程,以及數(shù)據(jù)標記與壓縮數(shù)據(jù)塊的三種對應關系的角度展開介紹。

分區(qū)、索引、標記和壓縮數(shù)據(jù)核心組件介紹

MergeTree引擎存儲結(jié)構(gòu)

MergeTree的存儲結(jié)構(gòu)

  • partition:分區(qū)目錄,余下各類數(shù)據(jù)文件(primary.idx、[Column].mrk、[Column]. bin等)都是以分區(qū)目錄的形式被組織存放的,屬于相同分區(qū)的數(shù)據(jù),最終會被合并到同一個分區(qū)目錄,而不同分區(qū)的數(shù)據(jù),永遠不會被合并在一起。
  • checksums:校驗文件,使用二進制格式存儲。它保存了余下各類文件(primary. idx、count.txt等)的size大小及size的哈希值,用于快速校驗文件的完整性和正確性。
  • columns.txt:列信息文件,使用明文格式存儲,用于保存此數(shù)據(jù)分區(qū)下的列字段信息。
  • count.txt:計數(shù)文件,使用明文格式存儲,用于記錄當前數(shù)據(jù)分區(qū)目錄下數(shù)據(jù)的總行數(shù)。
  • primary.idx:一級索引文件,使用二進制格式存儲。用于存放稀疏索引,一張MergeTree表只能聲明一次一級索引。借助稀疏索引,在數(shù)據(jù)查詢的時能夠排除主鍵條件范圍之外的數(shù)據(jù)文件,從而有效減少數(shù)據(jù)掃描范圍,加速查詢速度。
  • [Column].bin:數(shù)據(jù)文件,使用壓縮格式存儲,用于存儲某一列的數(shù)據(jù)。由于MergeTree采用列式存儲,所以每一個列字段都擁有獨立的.bin數(shù)據(jù)文件,并以列字段名稱命名。
  • [Column].mrk:使用二進制格式存儲。標記文件中保存了.bin文件中數(shù)據(jù)的偏移量信息。標記文件與稀疏索引對齊,又與.bin文件一一對應,所以MergeTree通過標記文件建立了primary.idx稀疏索引與.bin數(shù)據(jù)文件之間的映射關系。即首先通過稀疏索引(primary.idx)找到對應數(shù)據(jù)的偏移量信息(.mrk),再通過偏移量直接從.bin文件中讀取數(shù)據(jù)。由于.mrk標記文件與.bin文件一一對應,所以MergeTree中的每個列字段都會擁有與其對應的.mrk標記文件
  • [Column].mrk2:如果使用了自適應大小的索引間隔,則標記文件會以.mrk2命名。它的工作原理和作用與.mrk標記文件相同。
  • partition.dat與minmax_[Column].idx:如果使用了分區(qū)鍵,例如PARTITION BY EventTime,則會額外生成partition.dat與minmax索引文件,它們均使用二進制格式存儲。partition.dat用于保存當前分區(qū)下分區(qū)表達式最終生成的值;而minmax索引用于記錄當前分區(qū)下分區(qū)字段對應原始數(shù)據(jù)的最小和最大值。
  • skp_idx_[Column].idx與skp_idx_[Column].mrk:如果在建表語句中聲明了二級索引,則會額外生成相應的二級索引與標記文件,它們同樣也使用二進制存儲。二級索引在ClickHouse中又稱跳數(shù)索引。

分區(qū)

在MergeTree中,數(shù)據(jù)是以分區(qū)目錄的形式進行組織的,每個分區(qū)獨立分開存儲: Partition_1, Partition_2, Partition_3, Partition_4, .....

借助這種形式,在對MergeTree進行數(shù)據(jù)查詢時,可以有效跳過無用的數(shù)據(jù)文件,只使用最小的分區(qū)目錄子集。

數(shù)據(jù)的分區(qū)規(guī)則

MergeTree數(shù)據(jù)分區(qū)的規(guī)則由分區(qū)ID決定,而具體到每個數(shù)據(jù)分區(qū)所對應的ID,則是由分區(qū)鍵的取值決定的。分區(qū)鍵支持使用任何一個或一組字段表達式聲明,其業(yè)務語義可以是年、月、日或者組織單位等任何一種規(guī)則。針對取值數(shù)據(jù)類型的不同,分區(qū)ID的生成邏輯目前擁有四種規(guī)則:

(1)不指定分區(qū)鍵:如果不使用分區(qū)鍵,即不使用PARTITION BY聲明任何分區(qū)表達式,則分區(qū)ID默認取名為all,所有的數(shù)據(jù)都會被寫入這個all分區(qū)。
(2)使用整型:如果分區(qū)鍵取值屬于整型(兼容UInt64,包括有符號整型和無符號整型),且無法轉(zhuǎn)換為日期類型YYYYMMDD格式,則直接按照該整型的字符形式輸出,作為分區(qū)ID的取值。
(3)使用日期類型:如果分區(qū)鍵取值屬于日期類型,或者是能夠轉(zhuǎn)換為YYYYMMDD格式的整型,則使用按照YYYYMMDD進行格式化后的字符形式輸出,并作為分區(qū)ID的取值。
(4)使用其他類型:如果分區(qū)鍵取值既不屬于整型,也不屬于日期類型,例如String、Float等,則通過128位Hash算法取其Hash值作為分區(qū)ID的取值。數(shù)據(jù)在寫入時,會對照分區(qū)ID落入相應的數(shù)據(jù)分區(qū)

partition:分區(qū)目錄,里面的各類數(shù)據(jù)文件(primary.idx、data.mrk、data.bin 等等)都是以分區(qū)目錄的形式被組織存放的,屬于相同分區(qū)的數(shù)據(jù),最終會被合并到同一個分區(qū)目錄,而不同分區(qū)的數(shù)據(jù)永遠不會被合并在一起。

分區(qū)目錄的命名規(guī)則是:PartitionID_MinBlockNum_MaxBlockNum_Level

下面來解釋一下這幾個部分:

1)PartitionID:分區(qū) ID,這個應該無需多說。

2)MinBlockNum、MaxBlockNum:最小數(shù)據(jù)塊編號和最大數(shù)據(jù)塊編號,這里的命名很容易讓人聯(lián)想到后面要說的數(shù)據(jù)壓縮塊,甚至產(chǎn)生混淆,但實際上這兩者沒有任何關系。這里的 BlockNum 是一個自增的整數(shù),從 1 開始,每當創(chuàng)建一個新的分區(qū)時就會自增 1,并且對于一個新的分區(qū)目錄而言,它的 MinBlockNum 和 MaxBlockNum 是相等的。比如 202005_1_1_0、202006_2_2_0、202007_3_3_0,以此類推。但是也有例外,當分區(qū)目錄發(fā)生合并的時候,那么其 MinBlockNum 和 MaxBlockNum 會有另外的規(guī)則,一會兒細說。

3)Level:合并的層級,可以理解為某個分區(qū)被合并的次數(shù),這里的 Level 和 BlockNum 不同,它不是全局累加的。對于每個新創(chuàng)建的目錄而言,其初始值都為 0,之后以分區(qū)為單位,如果相同分區(qū)發(fā)生合并動作,則該分區(qū)對應的 Level 加 1。可能有人不是很理解這里的 "相同分區(qū)發(fā)生合并" 到底是什么意思,我們下面就來介紹。

分區(qū)目錄的合并過程

MergeTree 的分區(qū)目錄和其它傳統(tǒng)意義上數(shù)據(jù)庫有所不同,首先 MergeTree 的分區(qū)目錄并不是在數(shù)據(jù)表被創(chuàng)建之后就存在的,而是在數(shù)據(jù)寫入的過程中被創(chuàng)建的,如果一張表中沒有任何數(shù)據(jù),那么也就不會有任何的分區(qū)目錄。也很好理解,因為分區(qū)目錄的命名與分區(qū) ID 有關,而分區(qū) ID 又和分區(qū)鍵對應的值有關,而表中連數(shù)據(jù)都沒有,那么何來分區(qū)目錄呢。

其次,MergeTree 的分區(qū)目錄也不是一成不變的,在其它數(shù)據(jù)庫的設計中,追加數(shù)據(jù)的時候目錄自身不會改變,只是在相同分區(qū)中追加數(shù)據(jù)文件。而 MergeTree 完全不同,伴隨著每一次數(shù)據(jù)的寫入,MergeTree 都會生成一批新的分區(qū)目錄,即使不同批次寫入的數(shù)據(jù)屬于相同的分區(qū),也會生成不同的分區(qū)目錄。也就是說對于同一個分區(qū)而言,會存在對應多個分區(qū)目錄的情況。而在之后的某個時刻(一般 10 到 15 分鐘),ClickHouse 會通過后臺任務將屬于相同分區(qū)的多個目錄合并(Merge)成一個新的目錄,當然也可以通過 optimize TABLE table_name FINAL 語句立即合并,至于合并之前的舊目錄會在之后的某個時刻(默認 8 分鐘)被刪除。

屬于同一個分區(qū)的多個目錄,在合并之后會生成一個全新的目錄,目錄中的索引和數(shù)據(jù)文件也會相應地進行合并。而新目錄的名稱的生成方式遵循如下規(guī)則:

1.PartitionID:不變
2.MinBlockNum:取同一分區(qū)內(nèi)所有目錄中最小的 MinBlockNum
3.MaxBlockNum:取同一分區(qū)內(nèi)所有目錄中最大的 MaxBlockNum
4.Level:取同一分區(qū)內(nèi)最大 Level 值并加 1

這里有一點需要明確,在 ClickHouse中,數(shù)據(jù)分區(qū)(partition)和數(shù)據(jù)分片(shard)是完全不同的概念。數(shù)據(jù)分區(qū)是針對本地數(shù)據(jù)而言的,是對數(shù)據(jù)的一種縱向切分。MergeTree并不能依靠分區(qū)的特性,將一張表的數(shù)據(jù)分布到多個ClickHouse服務節(jié)點。而橫向切分是數(shù)據(jù)分片(shard)的能力。

索引

一級索引

primary.idx:一級索引文件,使用二進制格式存儲,用于存儲稀疏索引,一張 MergeTree 表只能聲明一次一級索引(通過 ORDER BY 或 PRIMARY KEY)。借助稀疏索引,在查詢數(shù)據(jù)時能夠排除主鍵條件范圍之外的數(shù)據(jù)文件,從而有效減少數(shù)據(jù)掃描范圍,加速查詢速度。

一級索引底層采用了稀疏索引來實現(xiàn),從下圖我們可以看出它和稠密索引的區(qū)別。

稀疏索引與稠密索引的對比圖

對于稠密索引而言,每一行索引標記都會對應到具體的一行記錄上。而在稀疏索引中,每一行索引標記對應的一大段數(shù)據(jù),而不是具體的一行(他們之間的區(qū)別就有點類似mysql中innodb的聚集索引與非聚集索引)。

稀疏索引的優(yōu)勢是顯而易見的,它只需要使用少量的索引標記就能夠記錄大量數(shù)據(jù)的區(qū)間位置信息,并且數(shù)據(jù)量越大優(yōu)勢愈發(fā)明顯。例如我們使用默認的索引粒度(8192)時,MergeTree只需要12208行索引標記就能為1億行數(shù)據(jù)記錄提供索引。由于稀疏索引占用空間小,所以primary.idx內(nèi)的索引數(shù)據(jù)能夠常駐內(nèi)存,取用速度自然極快。

索引粒度 index_granularity

索引粒度就如同標尺一般,會丈量整個數(shù)據(jù)的長度,并依照刻度對數(shù)據(jù)進行標注,最終將數(shù)據(jù)標記成多個間隔的小段。數(shù)據(jù)以index_granularity的粒度(老版本默認8192,新版本實現(xiàn)了自適應粒度)被標記成多個小的區(qū)間,其中每個區(qū)間最多8192行數(shù)據(jù),MergeTree使用MarkRange表示一個具體的區(qū)間,并通過start和end表示其具體的范圍。如下圖所示。

索引粒度是建表的時候,在 SETTINGS 里面指定 index_granularity 控制的,雖然 ClickHouse 提供了自適應粒度大小的特性,但是為了便于理解,我們會使用固定的索引粒度進行介紹(8192)。索引粒度對于 MergeTree 而言是一個非常重要的概念,它就如同一把標尺,會丈量整個數(shù)據(jù)的長度,并依照刻度對數(shù)據(jù)進行標注,最終將數(shù)據(jù)標記成多個間隔的小段。

數(shù)據(jù)以 index_granularity 的粒度(默認 8192)被標記成多個小的區(qū)間,其中每個區(qū)間最多 8192 行數(shù)據(jù),MergeTree 使用 MarkRange 表示一個具體的區(qū)間,并通過 start 和 end 表示其具體的范圍。index_granularity 的名字雖然取了索引二字,但它不單單只作用于一級索引,同時還會影響數(shù)據(jù)標記文件(data.mrk)和數(shù)據(jù)文件(data.bin)。因為只有一級索引是無法完成查詢工作的,它需要借助標記文件中的偏移量才能定位數(shù)據(jù),所以一級索引和數(shù)據(jù)標記的間隔粒度(同為 index_granularity 行)相同,彼此對齊,而數(shù)據(jù)文件也會按照 index_granularity 的間隔粒度生成壓縮數(shù)據(jù)塊。

二級索引

skp_idx_[IndexName].idx 和 skp_idx_[IndexName].mrk3:如果在建表語句中指定了二級索引,則會額外生成相應的二級索引文件與標記文件,它們同樣使用二進制存儲。二級索引在 ClickHouse 中又被稱為跳數(shù)索引,目前擁有 minmax、set、ngrambf_v1 和 token_v1 四種類型,這些種類的跳數(shù)索引的目的和一級索引都相同,都是為了進一步減少數(shù)據(jù)的掃描范圍,從而加速整個查詢過程。

標記

如果把MergeTree比作一本書,primary.idx 一級索引好比這本書的一級章節(jié)目錄,.bin文件中的數(shù)據(jù)好比這本書中的文字,那么數(shù)據(jù)標記(.mrk) 會為一級章節(jié)目錄和具體的文字之間建立關聯(lián) ( 書簽 )。對于數(shù)據(jù)標記而言,它記錄了兩點重要信息:

其一,是一級章節(jié)對應的頁碼信息;

其二,是一段文字在某一頁中的起始位置信息。

這樣一來,通過數(shù)據(jù)標記就能夠很快地從一本書中立即翻到關注內(nèi)容所在的那一頁,并知道從第幾行開始閱讀。

data.mrk:標記文件

使用二進制格式存儲,標記文件中保存了 data.bin 文件中數(shù)據(jù)的偏移量信息,并且標記文件與稀疏索引對齊,因此 MergeTree 通過標記文件建立了稀疏索引(primary.idx)與數(shù)據(jù)文件(data.bin)之間的映射關系。而在讀取數(shù)據(jù)的時候,首先會通過稀疏索引(primary.idx)找到對應數(shù)據(jù)的偏移量信息(data.mrk),因為兩者是對齊的,然后再根據(jù)偏移量信息直接從 data.bin 文件中讀取數(shù)據(jù)。

data.mrk3:如果使用了自適應大小的索引間隔,則標記文件會以 data.mrk3 結(jié)尾,但它的工作原理和 data.mrk 文件是相同的。

數(shù)據(jù)標記作為銜接一級索引和數(shù)據(jù)的橋梁,像極了書簽,而且書本總每一個章節(jié)目錄都有各自的書簽。

從圖中我們可以看到,數(shù)據(jù)標記和索引區(qū)間是對齊的,均按照 index_granularity 的粒度間隔,如此一來只需要簡單通過索引下標編號即可直接找到對應的數(shù)據(jù)標記。并且為了能夠與數(shù)據(jù)銜接,.bin 文件和數(shù)據(jù)標記文件是一一對應的,即每一個 [Column].bin 文件都有一個 [Column].mrk 數(shù)據(jù)標記文件與之對應,用于記錄數(shù)據(jù)在 .bin 文件中的偏移量信息。

一行標記數(shù)據(jù)使用一個元組表示,元組內(nèi)包含兩個整型數(shù)值的偏移量信息,分別表示在此段數(shù)據(jù)區(qū)間內(nèi):

  • 1\. 對應 .bin 壓縮文件中,壓縮數(shù)據(jù)塊的起始偏移量
  • 2\. 將該數(shù)據(jù)塊解壓縮后,未壓縮數(shù)據(jù)的起始偏移量

一行標記數(shù)據(jù)使用一個元組表示,元組內(nèi)包含兩個整型數(shù)值的偏移量信息。它們分別表示在此段數(shù)據(jù)區(qū)間內(nèi),在對應的.bin壓縮文件中,壓縮數(shù)據(jù)塊的起始偏移量;以及將該數(shù)據(jù)壓縮塊解壓后,其未壓縮數(shù)據(jù)的起始偏移量。圖所示是.mrk文件內(nèi)標記數(shù)據(jù)的示意。

每一行標記數(shù)據(jù)都表示了一個片段的數(shù)據(jù)(默認8192行)在.bin壓縮文件中的讀取位置信息。標記數(shù)據(jù)與一級索引數(shù)據(jù)不同,它并不能常駐內(nèi)存,而是使用LRU(最近最少使用)緩存策略加快其取用速度。

壓縮數(shù)據(jù)

數(shù)據(jù)量比較少,每一列數(shù)據(jù)的大小不是很大,因此每一列只用一個壓縮數(shù)據(jù)塊即可存儲。如果數(shù)據(jù)量再多一些,一個壓縮數(shù)據(jù)塊存儲不下,那么就會對應多個壓縮數(shù)據(jù)塊。

Column1 壓縮數(shù)據(jù)塊0
Column2 壓縮數(shù)據(jù)塊0
Column3 壓縮數(shù)據(jù)塊0
......
ColumnN 壓縮數(shù)據(jù)塊0

Column1 壓縮數(shù)據(jù)塊1
Column2 壓縮數(shù)據(jù)塊1
Column3 壓縮數(shù)據(jù)塊1
......
ColumnN 壓縮數(shù)據(jù)塊1

Column1 壓縮數(shù)據(jù)塊2
Column2 壓縮數(shù)據(jù)塊2
Column3 壓縮數(shù)據(jù)塊2
......
ColumnN 壓縮數(shù)據(jù)塊2

Column1 壓縮數(shù)據(jù)塊3
Column2 壓縮數(shù)據(jù)塊3
Column3 壓縮數(shù)據(jù)塊3
......

壓縮數(shù)據(jù)塊

一個壓縮數(shù)據(jù)塊由頭信息和壓縮數(shù)據(jù)兩部分組成。頭信息固定使用9位字節(jié)表示,具體由1個UInt8(1字節(jié))整型和2個UInt32(4字節(jié))整型組成,分別代表使用的壓縮算法類型、壓縮后的數(shù)據(jù)大小和壓縮前的數(shù)據(jù)大小。

從圖所示中能夠看到,.bin壓縮文件是由多個壓縮數(shù)據(jù)塊組成的,而每個壓縮數(shù)據(jù)塊的頭信息則是基于CompressionMethod_CompressedSize_UncompressedSize公式生成的。通過ClickHouse提供的clickhouse-compressor工具,能夠查詢某個.bin文件中壓縮數(shù)據(jù)的統(tǒng)計信息。

一個 .bin 文件是由1至多個壓縮數(shù)據(jù)塊組成的,每個壓縮塊大小在64KB~1MB之間。多個壓縮數(shù)據(jù)塊之間,按照寫入順序首尾相接,緊密地排列在一起。

在 .bin 文件中引入壓縮數(shù)據(jù)塊的目的至少有以下兩個:

其一,雖然數(shù)據(jù)被壓縮后能夠有效減少數(shù)據(jù)大小,降低存儲空間并加速數(shù)據(jù)傳輸效率,但數(shù)據(jù)的壓縮和解壓動作,其本身也會帶來額外的性能損耗。所以需要控制被壓縮數(shù)據(jù)的大小,以求在性能損耗和壓縮率之間尋求一種平衡。

其二,在具體讀取某一列數(shù)據(jù)時(.bin文件),首先需要將壓縮數(shù)據(jù)加載到內(nèi)存并解壓,這樣才能進行后續(xù)的數(shù)據(jù)處理。通過壓縮數(shù)據(jù)塊,可以在不讀取整個.bin文件的情況下將讀取粒度降低到壓縮數(shù)據(jù)塊級別,從而進一步縮小數(shù)據(jù)讀取的范圍。

分區(qū)索引 minmax_[Column].idx

partition.dat 和 minmax_[Column].idx:如果使用了分區(qū)鍵,例如上面的 PARTITION BY toYYYYMM(JoinTime),則會額外生成 partition.dat 與 minmax_JoinTime.idx 索引文件,它們均使用二進制格式存儲。

partition.dat 用于保存當前分區(qū)下分區(qū)表達式最終生成的值,而 minmax_[Column].idx 則負責記錄當前分區(qū)下分區(qū)字段對應原始數(shù)據(jù)的最小值和最大值。

數(shù)據(jù)Partitioning

ClickHouse支持PARTITION BY子句,在建表時可以指定按照任意合法表達式進行數(shù)據(jù)分區(qū)操作,比如通過toYYYYMM()將數(shù)據(jù)按月進行分區(qū)、toMonday()將數(shù)據(jù)按照周幾進行分區(qū)、對Enum類型的列直接每種取值作為一個分區(qū)等。

數(shù)據(jù)Partition在ClickHouse中主要有兩方面應用:

  • 在partition key上進行分區(qū)裁剪,只查詢必要的數(shù)據(jù)。靈活的partition expression設置,使得可以根據(jù)SQL Pattern進行分區(qū)設置,最大化的貼合業(yè)務特點。
  • 對partition進行TTL管理,淘汰過期的分區(qū)數(shù)據(jù)。

數(shù)據(jù)TTL

在分析場景中,數(shù)據(jù)的價值隨著時間流逝而不斷降低,多數(shù)業(yè)務出于成本考慮只會保留最近幾個月的數(shù)據(jù),ClickHouse通過TTL提供了數(shù)據(jù)生命周期管理的能力。

ClickHouse支持幾種不同粒度的TTL:

1) 列級別TTL:當一列中的部分數(shù)據(jù)過期后,會被替換成默認值;當全列數(shù)據(jù)都過期后,會刪除該列。

2)行級別TTL:當某一行過期后,會直接刪除該行。

3)分區(qū)級別TTL:當分區(qū)過期后,會直接刪除該分區(qū)。

數(shù)據(jù)寫入過程

分區(qū)目錄、索引、標記和壓縮數(shù)據(jù)的生成過程示意圖如下:

生成分區(qū)目錄

數(shù)據(jù)寫入的第一步是生成分區(qū)目錄,伴隨著每一批數(shù)據(jù)的寫入,都會生成一個新的分區(qū)目錄。在后續(xù)的某一時刻,屬于相同分區(qū)的目錄會依照規(guī)則合并到一起。

生成索引

按照index_granularity索引粒度,會分別生成primary.idx主鍵索引(如果聲明了二級索引,還會創(chuàng)建二級索引文件)。

生成標記和數(shù)據(jù)壓縮文件

按照index_granularity索引粒度,分別生成每一個列字段的.mrk數(shù)據(jù)標記和.bin壓縮數(shù)據(jù)文件。

ClickHouse 數(shù)據(jù)查詢流程

數(shù)據(jù)查詢概述

數(shù)據(jù)查詢的本質(zhì),可以看作一個不斷減小數(shù)據(jù)范圍的過程。在最理想的情況下,MergeTree首先可以依次借助分區(qū)索引、一級索引和二級索引,將數(shù)據(jù)掃描范圍縮至最小。然后再借助數(shù)據(jù)標記,將需要解壓與計算的數(shù)據(jù)范圍縮至最小。

如果一條查詢語句沒有指定任何WHERE條件,或是指定了WHERE條件,但條件沒有匹配到任何索引(分區(qū)索引、一級索引和二級索引),那么MergeTree就不能預先減小數(shù)據(jù)范圍。

在后續(xù)進行數(shù)據(jù)查詢時,它會掃描所有分區(qū)目錄,以及目錄內(nèi)索引段的最大區(qū)間。雖然不能減少數(shù)據(jù)范圍,但是MergeTree仍然能夠借助數(shù)據(jù)標記,以多線程的形式同時讀取多個壓縮數(shù)據(jù)塊,以提升性能。

索引的查詢過程

索引查詢其實就是兩個數(shù)值區(qū)間的交集判斷。

其中,一個區(qū)間是由基于主鍵的查詢條件轉(zhuǎn)換而來的條件區(qū)間;而另一個區(qū)間是剛才所講述的與MarkRange對應的數(shù)值區(qū)間。下圖簡要描述了 Id 字段的索引過程。

整個索引的查詢過程可以分為三大步驟

1.生成查詢條件區(qū)間:將查詢條件轉(zhuǎn)換為條件區(qū)間。即便是單個值的查詢條件,也會被轉(zhuǎn)換成區(qū)間的形式。

WHERE ID = 'A000' 
= ['A000', 'A000']

WHERE ID > 'A000'
= ('A000', '+inf')

WHERE ID < 'A000' 
= ('-inf', 'A000')

WHERE ID LIKE 'A000%' 
= ['A000', 'A001')

2.遞歸交集判斷:以遞歸的形式,依次對MarkRange的數(shù)值區(qū)間與條件區(qū)間做交集判斷。從最大的區(qū)間[A000 , +inf)開始。

(1)如果不存在交集,則直接通過剪枝算法優(yōu)化此整段MarkRange
(2)如果存在交集,且MarkRange步長大于N,則將這個區(qū)間進一步拆分為N個子區(qū)間,并重復此規(guī)則,(3)繼續(xù)做遞歸交集判斷(N由merge_tree_coarse_index_granularity指定,默認值為8), 如果存在交集,且MarkRange不可再分解,則記錄MarkRange并返回.

3.合并MarkRange區(qū)間:將最終匹配的MarkRange聚在一起,合并它們的范圍。

總結(jié)

分區(qū)、索引、標記和壓縮數(shù)據(jù)的協(xié)同工作總結(jié)

分區(qū)、索引、標記和壓縮數(shù)據(jù),就類似于 MergeTree 的一套組合拳,使用恰當?shù)脑捦o窮。那么在依次介紹了各自的特點之后,現(xiàn)在將它們聚在一起總結(jié)一下。

寫入過程

數(shù)據(jù)寫入的第一步是生成分區(qū)目錄,伴隨著每一批數(shù)據(jù)的寫入,都會生成一個新的分區(qū)目錄。在后續(xù)的某一時刻,屬于相同分區(qū)的分區(qū)目錄會被合并到一起。緊接著按照 index_granularity 索引粒度,會分別生成 primary.idx 一級索引(如果聲明了二級索引,還會創(chuàng)建二級索引文件)、每一個列字段的壓縮數(shù)據(jù)文件(.bin)和數(shù)據(jù)標記文件(.mrk),如果數(shù)據(jù)量不大,則是 data.bin 和 data.mrk 文件。

下面的示意圖展示了 MergeTree 表在寫入數(shù)據(jù)時,它的分區(qū)目錄、索引、標記和壓縮數(shù)據(jù)的生成。

從分區(qū)目錄 202006_1_34_3 能夠得知,該分區(qū)數(shù)據(jù)總共分 34 批寫入,期間發(fā)生過 3 次合并。在數(shù)據(jù)寫入的過程中,依據(jù) index_granularity 的粒度,依次為每個區(qū)間的數(shù)據(jù)生成索引、標記和壓縮數(shù)據(jù)塊。其中索引和標記區(qū)間是對齊的,而標記與壓縮塊則是根據(jù)區(qū)間大小的不同,會生成多對一、一對一、一對多的關系。

查詢過程

數(shù)據(jù)查詢的本質(zhì)可以看做是一個不斷減少數(shù)據(jù)范圍的過程,在最理想的情況下,MergeTree 首先可以借助分區(qū)索引、一級索引和二級索引將數(shù)據(jù)掃描范圍縮至最小。然后再借助數(shù)據(jù)標記,將需要解壓與計算的數(shù)據(jù)范圍縮至最小。以下圖為例,該圖展示了在最優(yōu)的情況下,經(jīng)過層層過濾,最終獲取最小數(shù)據(jù)范圍的過程。

如果一條查詢語句沒有指定任何 WHERE 條件,或者指定了 WHERE 條件、但是沒有匹配到任何的索引(分區(qū)索引、一級索引、二級索引),那么 MergeTree 就不能預先減少數(shù)據(jù)范圍。在后續(xù)進行數(shù)據(jù)查詢時,它會掃描所有分區(qū)目錄,以及目錄內(nèi)索引段的最大區(qū)間。不過雖然不能減少數(shù)據(jù)范圍,但 MergeTree 仍然能夠借助數(shù)據(jù)標記,以多線程的形式同時讀取多個壓縮數(shù)據(jù)塊,以提升性能。

數(shù)據(jù)標記與壓縮數(shù)據(jù)塊的對應關系

由于壓縮數(shù)據(jù)塊的劃分,與一個間隔(index_granularity)內(nèi)的數(shù)據(jù)大小相關,每個壓縮數(shù)據(jù)塊的體積都被嚴格控制在 64KB ~ 1MB 之間,而一個間隔(index_granularity)的數(shù)據(jù),又只會產(chǎn)生一行數(shù)據(jù)標記。那么根據(jù)一個間隔內(nèi)數(shù)據(jù)的實際字節(jié)大小,數(shù)據(jù)標記和壓縮數(shù)據(jù)塊之間會產(chǎn)生三種不同的對應關系:

1)多對一

多個數(shù)據(jù)標記對應一個壓縮數(shù)據(jù)塊,當一個間隔(index_granularity)內(nèi)數(shù)據(jù)的未壓縮大小小于 64KB 時,會出現(xiàn)這種對應關系。

2)一對一

一個數(shù)據(jù)標記對應一個壓縮數(shù)據(jù)塊,當一個間隔(index_granularity)內(nèi)數(shù)據(jù)的未壓縮大小大于等于 64KB 并小于等于 1MB 時,會出現(xiàn)這種對應關系。

3)一對多

一個數(shù)據(jù)標記對應多個壓縮數(shù)據(jù)塊,當一個間隔(index_granularity)內(nèi)數(shù)據(jù)的未壓縮大小大于 1MB 時,會出現(xiàn)這種對應關系。

以上就是 MergeTree 的工作原理,首先我們了解了 MergeTree 的基礎屬性和物理存儲結(jié)構(gòu);接著,依次介紹了數(shù)據(jù)分區(qū)、一級索引、二級索引、數(shù)據(jù)存儲和數(shù)據(jù)標記的重要特性;最后總結(jié)了 MergeTree 上述特性一起協(xié)同時工作過程。掌握了 MergeTree 即掌握了合并樹系列表引擎的精髓,因為 MergeTree 本身也是一種表引擎。后面我們會介紹 MergeTree 家族中其它常見表引擎的使用方法,以及它們都有哪些特點、使用方式是什么。

參考資料

https://blog.csdn.net/Night_ZW/article/details/112845684

https://blog.csdn.net/qq_35423154/article/details/117160058

https://www.cnblogs.com/traditional/p/15218743.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容