1 總體介紹
Doris是基於MPP架構的交互式SQL數據倉庫,主要用於解決了近實時的報表和多維分析。Doris高效的導入、查詢離不開其存儲結構精巧的設計。本文主要經過閱讀Doris BE模塊代碼,詳細分析了Doris BE模塊存儲層的實現原理,闡述和解密Doris高效的寫入、查詢能力背後的核心技術。其中包括Doris列存的設計、索引設計、數據讀寫流程、Compaction流程等功能。這裏會經過三篇文章來逐步進行介紹,分別爲《Doris存儲層設計介紹1——存儲結構設計解析》、《Doris存儲層設計介紹2——寫入流程、刪除流程分析》、《Doris存儲層設計介紹3——讀取、Compaction流程分析》。html
本文爲第一篇《Doris存儲層設計介紹1——存儲結構設計解析》,文章介紹了Segment V2版本的存儲層結構,包括了有序存儲、稀疏索引、前綴索引、位圖索引、BloomFilter等豐富功能,能夠應對各類複雜的場景提供極速的查詢能力。git
2 設計目標
- 批量導入,少許更新
- 絕大多數的讀請求
- 寬表場景,讀取大量行,少許列
- 非事務場景
- 良好的擴展性
3 存儲文件格式
3.1 存儲目錄結構
存儲層對存儲數據的管理經過storage_root_path路徑進行配置,路徑能夠是多個。存儲目錄下一層按照分桶進行組織,分桶目錄下存放具體的tablet,按照tablet_id命名子目錄。github
Segment文件存放在tablet_id目錄下按SchemaHash管理。Segment文件能夠有多個,通常按照大小進行分割,默認爲256MB。其中,Segment v2文件命名規則爲:${rowset_id}_${segment_id}.dat。具體存儲目錄存放格式以下圖所示:算法
3.2 Segment v2文件結構
Segment總體的文件格式分爲數據區域,索引區域和footer三個部分,以下圖所示:sql
- Data Region:用於存儲各個列的數據信息,這裏的數據是按需分page加載的
- Index Region: Doris中將各個列的index數據統一存儲在Index Region,這裏的數據會按照列粒度進行加載,因此跟列的數據信息分開存儲
- Footer信息
- SegmentFooterPB:定義文件的元數據信息
- 4個字節的FooterPB內容的checksum
- 4個字節的FileFooterPB消息長度,用於讀取FileFooterPB
- 8個字節的MAGIC CODE,之因此在末位存儲,是方便不一樣的場景進行文件類型的識別
下面分佈介紹各個部分的存儲格式的設計。apache
4 Footer信息
Footer信息段在文件的尾部,存儲了文件的總體結構,包括數據域的位置,索引域的位置等信息,其中有SegmentFooterPB,CheckSum,Length,MAGIC CODE 4個部分。微信
SegmentFooterPB數據結構以下:數據結構
SegmentFooterPB採用了PB格式進行存儲,主要包含了列的meta信息、索引的meta信息,Segment的short key索引信息、總行數。架構
4.1 列的meta信息
- ColumnId:當前列在schema中的序號
- UniqueId:全局惟一的id
- Type:列的類型信息
- Length:列的長度信息
- Encoding:編碼格式
- Compression:壓縮格式
- Dict PagePointer:字典信息
4.2 列索引的meta信息
- OrdinalIndex:存放列的稀疏索引meta信息。
- ZoneMapIndex:存放ZoneMap索引的meta信息,內容包括了最大值、最小值、是否有空值、是否沒有非空值。SegmentZoneMap存放了全局的ZoneMap信息,PageZoneMaps則存放了每一個頁面的統計信息。
- BitMapIndex:存放BitMap索引的meta信息,內容包括了BitMap類型,字典數據BitMap數據。
- BloomFilterIndex:存放了BloomFilter索引信息。
爲了防止索引自己數據量過大,ZoneMapIndex、BitMapIndex、BloomFilterIndex採用了兩級的Page管理。對應了IndexColumnMeta的結構,當一個Page可以放下時,當前Page直接存放索引數據,即採用1級結構;當一個Page沒法放下時,索引數據寫入新的Page中,Root Page存儲數據Page的地址信息。性能
5 Ordinal Index(一級索引)
Ordinal Index索引提供了經過行號來查找Column Data Page數據頁的物理地址。Ordinal Index可以將按列存儲數據按行對齊,能夠理解爲一級索引。其餘索引查找數據時,都要經過Ordinal Index查找數據Page的位置。所以,這裏先介紹Ordinal Index索引。
在一個segment中,數據始終按照key(AGGREGATE KEY、UNIQ KEY 和 DUPLICATE KEY)排序順序進行存儲,即key的排序決定了數據存儲的物理結構。肯定了列數據的物理結構順序,在寫入數據時,Column Data Page是由Ordinal index進行管理,Ordinal index記錄了每一個Column Data Page的位置offset、大小size和第一個數據項行號信息,即Ordinal。這樣每一個列具備按行信息進行快速掃描的能力。Ordinal index採用的稀疏索引結構,就像是一本書目錄,記錄了每一個章節對應的頁碼。
5.1 存儲結構
Ordinal index元信息存儲在SegmentFooterPB中的每一個列的OrdinalIndexMeta中。具體結構以下圖所示:
在OrdinalIndexMeta中存放了索引數據對應的root page地址,這裏作了一些優化,當數據僅有一個page時,這裏的地址能夠直接指向惟一的數據page;當一個page放不下時,指向OrdinalIndex類型的二級結構索引page,索引數據中每一個數據項對應了Column Data Page offset位置、size大小和ordinal行號信息。其中Ordinal index索引粒度與page粒度一致,默認64*1024字節。
六、列數據存儲
Column的data數據按照Page爲單位分塊存儲,每一個Page大小通常爲64*1024個字節。Page在存儲的位置和大小由ordinal index管理。
6.1 data page存儲結構
DataPage主要爲Data部分、Page Footer兩個部分。
Data部分存放了當前Page的列的數據。當容許存在Null值時,對空值單獨存放了Null值的Bitmap,由RLE格式編碼經過bool類型記錄Null值的行號。
Page Footer包含了Page類型Type、UncompressedSize未壓縮時的數據大小、FirstOrdinal當前Page第一行的RowId、NumValues爲當前Page的行數、NullMapSize對應了NullBitmap的大小。
6.2 數據壓縮
針對不一樣的字段類型採用了不一樣的編碼。默認狀況下,針對不一樣類型採用的對應關係以下:
TINYINT/SMALLINT/INT/BIGINT/LARGEINT | BIT_SHUFFLE |
FLOAT/DOUBLE/DECIMAL | BIT_SHUFFLE |
CHAR/VARCHAR | DICT |
BOOL | RLE |
DATE/DATETIME | BIT_SHUFFLE |
HLL/OBJECT | PLAIN |
默認採用LZ4F格式對數據進行壓縮。
七、Short Key Index索引
7.1 存儲結構
Short Key Index前綴索引,是在key(AGGREGATE KEY、UNIQ KEY 和 DUPLICATE KEY)排序的基礎上,實現的一種根據給定前綴列,快速查詢數據的索引方式。這裏Short Key Index索引也採用了稀疏索引結構,在數據寫入過程當中,每隔必定行數,會生成一個索引項。這個行數爲索引粒度默認爲1024行,可配置。該過程以下圖所示:
其中,KeyBytes中存放了索引項數據,OffsetBytes存放了索引項在KeyBytes中的偏移。
7.2 索引生成規則
Short Key Index採用了前36 個字節,做爲這行數據的前綴索引。當遇到 VARCHAR 類型時,前綴索引會直接截斷。
7.3 應用案例
(1)如下表結構的前綴索引爲 user_id(8Byte) + age(4Bytes) + message(prefix 24 Bytes)。
ColumnName | Type |
user_id | BIGINT |
age | INT |
message | VARCHAR(100) |
max_dwell_time | DATETIME |
min_dwell_time | DATATIME |
(2)如下表結構的前綴索引爲 user_name(20 Bytes)。即便沒有達到 36 個字節,由於遇到 VARCHAR,因此直接截斷,再也不日後繼續。
Column | Type |
user_name | VARCHAR(20) |
age | INT |
message | VARCHAR(100) |
max_dwell_time | DATETIME |
min_dwell_time | DATETIME |
當咱們的查詢條件,是前綴索引的前綴時,能夠極大的加快查詢速度。好比在第一個例子中,咱們執行以下查詢:
SELECT * FROM table WHERE user_id=1829239 and age=20;
該查詢的效率會遠高於以下查詢:
SELECT * FROM table WHERE age=20;
因此在建表時,正確的選擇列順序,可以極大地提升查詢效率。
八、ZoneMap Index索引
ZoneMap索引存儲了Segment和每一個列對應每一個Page的統計信息。這些統計信息能夠幫助在查詢時提速,減小掃描數據量,統計信息包括了Min最大值、Max最小值、HashNull空值、HasNotNull不全爲空的信息。
8.1 存儲結構
ZoneMap索引存儲結構以下圖所示:
在SegmentFootPB結構中,每一列索引元數據ColumnIndexMeta中存放了當前列的ZoneMapIndex索引數據信息。ZoneMapIndex有兩個部分,SegmentZoneMap和PageZoneMaps。SegmentZoneMap存放了當前Segment全局的ZoneMap索引信息,PageZoneMaps存放了每一個Data Page的ZoneMap索引信息。
PageZoneMaps對應了索引數據存放的Page信息IndexedColumnMeta結構,目前實現上沒有進行壓縮,編碼方式也爲Plain。IndexedColumnMeta中的OrdinalIndexPage指向索引數據root page的偏移和大小,這裏一樣作了優化二級Page優化,當僅有一個DataPage時,OrdinalIndexMeta直接指向這個DataPage;有多個DataPage時,OrdinalIndexMeta先指向OrdinalIndexPage,OrdinalIndexPage是一個二級Page結構,裏面的數據項爲索引數據DataPage的地址偏移offset,大小Size和ordinal信息。
8.2 索引生成規則
Doris默認爲key列開啓ZoneMap索引;當表的模型爲DUPULCATE時,會全部字段開啓ZoneMap索引。在列數據寫入Page時,自動對數據進行比較,不斷維護當前Segment的ZoneMap和當前Page的ZoneMap索引信息。
8.3 應用案例
在數據查詢時,會根據範圍條件過濾的字段會按照ZoneMap統計信息選取掃描的數據範圍。例如在案例1中,對age字段進行過濾。查詢語句以下:
SELECT * FROM table WHERE age > 20 and age < 1000
在沒有命中Short Key Index的狀況下,會根據條件語句中age的查詢條件,利用ZoneMap索引找到應該掃描的數據ordinary範圍,減小要掃描的page數量。
九、BloomFilter
當一些字段不能利用Short Key Index而且字段存在區分度比較大時,Doris提供了BloomFilter索引。
9.一、存儲結構
BloomFilter的存儲結構以下圖所示:
BloomFilterIndex信息存放了生產的Hash策略、Hash算法和BloomFilter過對應的數據Page信息。Hash算法採用了HASH_MURMUR3,Hash策略採用了BlockSplitBloomFilter分塊實現策略,指望的誤判率fpp默認配置爲0.05。BloomFilter索引數據對應數據Page的存放與ZoneMapIndex相似,作了二級Page的優化,這裏再也不詳細闡述。
9.二、索引生成規則
BloomFilter按Page粒度生成,在數據寫入一個完整的Page時,Doris會根據Hash策略同時生成這個Page的BloomFilter索引數據。目前bloom過濾器不支持tinyint/hll/float/double類型,其餘類型均已支持。使用時須要在PROPERTIES中指定bloom_filter_columns要使用BloomFilter索引的字段。
9.3 應用案例
在數據查詢時,查詢條件在設置有bloom過濾器的字段進行過濾,當bloom過濾器沒有命中時表示該Page中沒有該數據,這樣能夠減小要掃描的page數量。
案例:table的schema以下:
ColumnName | Type |
user_id | BIGINT |
age | INT |
name | VARCHAR(20) |
city | VARCHAR(200) |
createtime | DATETIME |
這裏的查詢sql以下:
SELECT * FROM table WHERE name = '張三'
因爲name的區分度較大,爲了提高sql的查詢性能,對name數據增長了BloomFilter索引,PROPERTIES ( "bloom_filter_columns" = "name" )。在查詢時經過BloomFilter索引可以大量過濾掉Page。
十、Bitmap Index索引
Doris還提供了BitmapIndex用來加速數據的查詢。
10.一、存儲結構
Bitmap存儲格式以下:
BitmapIndex的meta信息一樣存放在SegmentFootPB中,BitmapIndex包含了三部分,BitMap的類型、字典信息DictColumn、位圖索引數據信息BitMapColumn。其中DictColumn、BitMapColumn都對應IndexedColumnData結構,分別存放了字典數據和索引數據的Page地址offset、大小size。這裏一樣作了二級page的優化,再也不具體闡述。
這裏與其餘索引存儲結構有差別的地方是DictColumn字典數據進行了LZ4F壓縮,在記錄二級Page偏移時存放的是Data Page中的第一個值。
10.二、索引生成規則
BitMap建立時須要經過 CREATE INDEX 進行建立。Bitmap的索引是整個Segment中的Column字段的索引,而不是爲每一個Page單獨生成一份。在寫入數據時,會維護一個map結構記錄下每一個key值對應的行號,並採用Roaring位圖對rowid進行編碼。主要結構以下:
生成索引數據時,首先寫入字典數據,將map結構的key值寫入到DictColumn中。而後,key對應Roaring編碼的rowid以字節方式將數據寫入到BitMapColumn中。
10.三、應用案例
在數據查詢時,對於區分度不大,列的基數比較小的數據列,能夠採用位圖索引進行優化。好比,性別,婚姻,地理信息等。
案例:table的schema以下:
ColumnName | Type |
user_id | BIGINT |
age | INT |
name | VARCHAR(20) |
city | VARCHAR(200) |
createtime | DATETIME |
這裏的查詢sql以下:
SELECT * FROM table WHERE city in ("北京", "上海")
因爲city的取值比較少,創建數據字典和位圖後,經過掃描位圖即可以快速查找出匹配行。而且位圖壓縮後,數據量自己較小,經過掃描較少數據變可以對整個列進行精確的匹配。
十一、索引的查詢流程
在查詢一個Segment中的數據時,根據執行的查詢條件,會對首先根據字段加索引的狀況對數據進行過濾。而後在進行讀取數據,總體的查詢流程以下:
- 首先,會按照Segment的行數構建一個row_bitmap,表示記錄那些數據須要進行讀取,沒有使用任何索引的狀況下,須要讀取全部數據。
- 當查詢條件中按前綴索引規則使用到了key時,會先進行ShortKey Index的過濾,能夠在ShortKey Index中匹配到的ordinal行號範圍,合入到row_bitmap中。
- 當查詢條件中列字段存在BitMap Index索引時,會按照BitMap索引直接查出符合條件的ordinal行號,與row_bitmap求交過濾。這裏的過濾是精確的,以後去掉該查詢條件,這個字段就不會再進行後面索引的過濾。
- 當查詢條件中列字段存在BloomFilter索引而且條件爲等值(eq,in,is)時,會按BloomFilter索引過濾,這裏會走完全部索引,過濾每個Page的BloomFilter,找出查詢條件能命中的全部Page。將索引信息中的ordinal行號範圍與row_bitmap求交過濾。
- 當查詢條件中列字段存在ZoneMap索引時,會按ZoneMap索引過濾,這裏一樣會走完全部索引,找出查詢條件能與ZoneMap有交集的全部Page。將索引信息中的ordinal行號範圍與row_bitmap求交過濾。
- 生成好row_bitmap以後,批量經過每一個Column的OrdinalIndex找到到具體的Data Page。
- 批量讀取每一列的Column Data Page的數據。在讀取時,對於有null值的page,根據null值位圖判斷當前行是不是null,若是爲null進行直接填充便可。
十二、總結
Doris目前採用了徹底的列存儲結構,並提供了豐富的索引應對不一樣查詢場景,爲Doris高效的寫入、查詢性能奠基了夯實的基礎。Doris存儲層設計靈活,將來還能夠進一步增長新的索引、強化數據刪除等功能。