時序資料庫 Apache-IoTDB 原始碼解析之檔案索引塊(五)
上一章聊到 TsFile 的檔案組成,以及資料塊的詳細介紹。詳情請見:
時序資料庫 Apache-IoTDB 原始碼解析之檔案資料塊(四)
打一波廣告,歡迎大家訪問IoTDB 倉庫,求一波 Star。
這一章主要想聊聊:
- TsFile索引塊的組成
- 索引塊的查詢過程
- 索引塊目前在做的改進項
索引塊
索引塊由兩大部分組成,其寫入的方式是從左到右寫入,也就是從檔案頭向檔案尾寫入。但讀出的方式是先讀出TsFileMetaData 再讀出 TsDeviceMetaDataList 中的具體一部分。我們按照讀取資料的順序介紹:
TsFileMetaData
TsFileMetaData屬於檔案的 1 級索引,用來索引 Device 是否存在、在哪裡等資訊,其中主要儲存了:
- DeviceMetaDataIndexMap:Map結構,Key 是裝置名,Value 是 TsDeviceMetaDataIndex ,儲存了包含哪些 Device(邏輯概念上的一個集合一段時間內的資料,例如前幾章我們講到的:張三、李四、王五)以及他們的開始時間及結束時間、在左側 TsDeviceMetaDataList 檔案塊中的偏移量等。
- MeasurementSchemaMap:Map結構,Key 是測點的一個全路徑,Value 是 measurementSchema ,儲存了包含的測點資料(邏輯概念上的某一類資料的集合,如體溫資料)的原資訊,如:壓縮方式,資料型別,編碼方式等。
- 最後是一個布隆過濾器,快速檢測某一個
時間序列
我們再回想 SQL :SELECT 體溫 FROM 王五 WHERE time = 1
。讀檔案的過程就應該是:
- 先用
布隆過濾器
判斷檔案內是否有王五的體溫
列,如果沒有,查詢下一個檔案。 - 從 DeviceMetaDataIndexMap 中找到
王五
的 TsDeviceMetaDataIndex ,從而得到了王五
王五
的 TsDeviceMetadata 讀出來。 - MeasurementSchemaMap 不用關注,主要是給 Spark 使用的,ChunkHeader 中也儲存了這些資訊。
TsDeviceMetaDataList
TsDeviceMetaDataList 屬於檔案的 2 級索引,用來索引具體的測點資料是不是存在、在哪裡等資訊。其中主要儲存了:
- ChunkGroupMetaData:ChunkGroup 的索引資訊,主要包含了每個 ChunkGroup 資料塊的起止位置以及包含的所有的測點元資訊(ChunkMetaData)。
- ChunkMetaData :Chunk 的索引資訊,主要包含了每個裝置的測點在檔案中的起止位置、開始結束時間、資料型別和預聚合資訊。
上面的例子中,從 TsFileMetadata 已經拿到了王五
的 TsDeviceMetadataIndex,這裡就可以直接讀出王五的 TsDeviceMetadata,並且遍歷裡邊的 ChunkGroupMetadata 中的 ChunkMetadata,找到體溫
對應的所有的 ChunkMetadata。通過預聚合資訊對時間過濾,判斷能否使用當前的 Chunk 或者能否直接使用預聚合資訊直接返回資料(等介紹到 server 的查詢引擎時候細聊)。
如果不能直接返回,因為 ChunkMetaData 包含了這個 Chunk 對應的檔案的偏移量,只需要使用 seek(offSet)
就會跳轉到資料塊,使用上一章介紹的讀取方法進行遍歷就完成了整個讀取。
預聚合資訊(Statistics)
文中多次提到了預聚合
在這裡詳細介紹一下它的資料結構。
// 所屬檔案塊的開始時間
private long startTime;
// 所屬檔案塊的結束時間
private long endTime;
// 所屬檔案塊的資料型別
private TSDataType tsDataType;
// 所屬檔案塊的最小值
private int minValue;
// 所屬檔案塊的最大值
private int maxValue;
// 所屬檔案塊的第一個值
private int firstValue;
// 所屬檔案塊的最後一個值
private int lastValue;
// 所屬檔案塊的所有值的和
private double sumValue;
這個結構主要儲存在 ChunkMetaData 和 PageHeader 中,這樣做的好處就是,你不必從硬碟中讀取具體的Page
或者 Chunk
的檔案內容就可以獲得最終的結果,例如:SELECT SUM(體溫) FROM 王五
,當定位到 ChunkMetaData 時,判斷能否直接使用這個 Statistics 資訊(具體怎麼判斷,之後會在介紹 server 時具體介紹),如果能使用,那麼直接返回 sumValue。這樣返回的速度,無論存了多少資料,它的聚合結果響應時間簡直就是 1 毫秒以內。
樣例資料
我們繼續使用上一章聊到的示例資料來展示。
時間戳 | 人名 | 體溫 | 心率 |
---|---|---|---|
1580950800 | 王五 | 36.7 | 100 |
1580950911 | 王五 | 36.6 | 90 |
完整的檔案資訊如下:
POSITION| CONTENT
-------- -------
0| [magic head] TsFile
6| [version number] 000002
// 資料塊開始
||||||||||||||||||||| [Chunk Group] of wangwu begins at pos 12, ends at pos 253, version:0, num of Chunks:2
12| [Chunk] of xinlv, numOfPoints:1, time range:[1580950800,1580950800], tsDataType:INT32,
[minValue:100,maxValue:100,firstValue:100,lastValue:100,sumValue:100.0]
| [marker] 1
| [ChunkHeader]
| 1 pages
121| [Chunk] of tiwen, numOfPoints:1, time range:[1580950800,1580950800], tsDataType:FLOAT,
[minValue:36.7,maxValue:36.7,firstValue:36.7,lastValue:36.7,sumValue:36.70000076293945]
| [marker] 1
| [ChunkHeader]
| 1 pages
230| [Chunk Group Footer]
| [marker] 0
| [deviceID] wangwu
| [dataSize] 218
| [num of chunks] 2
||||||||||||||||||||| [Chunk Group] of wangwu ends
// 索引塊開始
253| [marker] 2
254| [TsDeviceMetadata] of wangwu, startTime:1580950800, endTime:1580950800
| [startTime] 1580950800
| [endTime] 1580950800
| [ChunkGroupMetaData] of wangwu, startOffset12, endOffset253, version:0, numberOfChunks:2
| [ChunkMetaData] of xinlv, startTime:1580950800, endTime:1580950800, offsetOfChunkHeader:12, dataType:INT32, statistics:[minValue:100,maxValue:100,firstValue:100,lastValue:100,sumValue:100.0]
| [ChunkMetaData] of tiwen, startTime:1580950800, endTime:1580950800, offsetOfChunkHeader:121, dataType:FLOAT, statistics:[minValue:36.7,maxValue:36.7,firstValue:36.7,lastValue:36.7,sumValue:36.70000076293945]
446| [TsFileMetaData]
| [num of devices] 1
| [TsDeviceMetadataIndex] of wangwu, startTime:1580950800, endTime:1580950800, offSet:254, len:192
| [num of measurements] 2
| 2 key&measurementSchema
| [createBy isNotNull] false
| [totalChunkNum] 2
| [invalidChunkNum] 0
//布隆過濾器
| [bloom filter bit vector byte array length] 30
| [bloom filter bit vector byte array]
| [bloom filter number of bits] 256
| [bloom filter number of hash functions] 5
599| [TsFileMetaDataSize] 153
603| [magic tail] TsFile
609| END of TsFile
當執行: SELECT 體溫 FROM 王五
時:
- 從
599
開始讀,1 級索引長度為 153. 599 - 153 = 446
就是 1 級索引讀開始位置,並讀出 TsDeviceMetadataIndex of 王五,其中記錄了,王五裝置的 2 級索引的 offset 為 254.- 跳到
254
開始讀 2 級索引,找到 ChunkMetaData of 體溫, 其中記錄了體溫資料的 Chunk 的offset 為121
- 跳到
121
,這裡進入了資料塊,從121
讀取到230
,讀出的資料就全部是體溫資料。
改進項
1. 只讀投影列
前面第 3 步中,讀取 2 級索引時候,會將這個裝置下的所有測點資料全部讀出來,這依然不太符合只讀投影列的設計,所以在新的 TsFile 中,修改了 1級索引和 2 級索引的部分結構,使得讀出的資料更少,更高效。有興趣的同學可以關注 PR: Refactor TsFile #736
2. 檔案級 Statistics
在物聯網場景中經常會涉及到查詢某個裝置的最後狀態,比如:車聯網中,查詢車輛的末次位置( SELECT LAST(lat,lon) FROM VechicleID
),或者當前的點火、熄火狀態等 SELECT LAST(accStatus) FROM VechicleID
。
或者當某些分頁查詢等情況時候,經常會使用到 COUNT(*)
等操作,這些都非常符合 Statistics 結構,這些場景涉及到的索引設計也都會體現到新的 TsFile 索引改動中。
到此已經介紹完了檔案的整體結構,瞭解了大體的寫入和讀取過程,但是 TsFile 的 API 是如何設計的,怎樣在程式碼裡做一些特殊的功課,來繞過 Java 裝箱、GC 等問題呢?歡迎持續關注。。。。