DorisDB根據攝入數據和實際存儲數據之間的映射關系, 將數據表的明細表, 聚合表和更新表, 分別對應有明細模型, 聚合模型和更新模型。
- Aggregate (聚合模型) : 將表中的列分為了Key和Value兩種,數據會根據維度列進行分組,並對指標列進行聚合。
- Unique (唯一主鍵模型):
- Duplicate (明細模型)
其它提升效率的方式:
- 構建Rollup
- 前綴索引與 ROLLUP
Aggregate(聚合模型)
一個正常的模型它肯定會把明細的數據存儲在一個數據庫中,也就是存在Doris中。但是因為Doris它最早是給鳳巢的一個廣告報表做的,廣告報表有一個很大的特點,就是它只關心統計分析的結果,而不太關心明細的數據,所以Doris最早一代的數據模型,是一個聚合的模型。
聚合模型的特點就是將表中的列分為了Key和Value兩種。 Key 就是數據的維度列,比如時間,地區等等。 Value 則是數據的指標列,比如點擊量,花費等。每個指標列還會有自己的聚合函數,包括sum、min、max和bitmap_union 等。數據會根據維度列進行分組,並對指標列進行聚合。如下圖
表中的列按照是否設置了 AggregationType,分為 Key (維度列) 和 Value(指標列)。沒有設置 AggregationType 的,如 user_id、date、age ... 等稱為 Key,而設置了 AggregationType 的稱為 Value。
如果轉換成建表語句則如下(省略建表語句中的 Partition 和 Distribution 信息)
1 CREATE TABLE IF NOT EXISTS example_db.expamle_tbl 2 ( 3 `user_id` LARGEINT NOT NULL COMMENT "用戶id", 4 `date` DATE NOT NULL COMMENT "數據灌入日期時間", 5 `city` VARCHAR(20) COMMENT "用戶所在城市", 6 `age` SMALLINT COMMENT "用戶年齡", 7 `sex` TINYINT COMMENT "用戶性別", 8 `last_visit_date` DATETIME REPLACE DEFAULT "1970-01-01 00:00:00" COMMENT "用戶最后一次訪問時間", 9 `cost` BIGINT SUM DEFAULT "0" COMMENT "用戶總消費", 10 `max_dwell_time` INT MAX DEFAULT "0" COMMENT "用戶最大停留時間", 11 `min_dwell_time` INT MIN DEFAULT "99999" COMMENT "用戶最小停留時間", 12 ) 13 AGGREGATE KEY(`user_id`, `date`, `city`, `age`, `sex`) 14 ... /* 省略 Partition 和 Distribution 信息 */
聚合模型的數據在Doris的3種機制下都會發生聚和:
- 導入
- Compaction(合並)
- 查詢
數據導入:
- 原始數據在導入過程中,會根據表結構中的Key進行分組,相同Key的Value會根據表中定義的Aggregation Function進行聚合。如下圖
- 由於Doris采用的是MVCC(Multi-version Cocurrent Control,多版本並發控制)機制進行的並發控制,所以每一次新的導入都是一個新的版本。我們把這種版本稱為 Singleton。
Compaction:
- 不斷的導入新的數據后,盡管同一批次的數據在導入過程中已經發生了聚合,但不同版本之間的數據依舊存在維度列相同但是指標列並沒有被聚合的情況。這時候就需要通過Compaction機制進行二次聚合。
Compaction 的意思其實就是將不同版本的數據進行合並。它 分為兩個階段:
- 第一個階段是: 當Singleton的數據版本個數到達Doris設置的閾值時,就會觸發 Cumulative 級別的Compaction。 這個級別的Compaction會將一個區間段內的版本數據根據定義好的聚合函數進行再聚合。
- 例如: Cumulative Compaction 會將61~70的這10個Singleton版本的數據合並成一個版本。這個版本的范圍就是61~70。
- 經過Cumulative Compaction后的數據已經合並多個區間內的版本,但並沒有最終合並成一個版本。這時候就需要 Base Compaction 來對已經完成Cumulative Compaction的版本做一個最終的合並。
- 例如: 上圖中的Base Compaction已經完成后第0個版本到第60個版本的聚合。那么下一次的Base Compaction 就是把61~70這個已經完成Cumulative合並的版本和 0~60版本再進行一個Base Compaction,最終生成一個0~70的版本。
查詢:
- 由於Compaction是異步后台執行的,在用戶查詢之前數據還並未合並在同一個版本中,而是存在於多個版本中的。所以用戶查詢數據的時候,為了保證查詢結果的正確性,Doris會把從0到當前最新版本的數據都讀出來然后再做一次聚合,最后將結果返回給用戶。
-
例如: 有一個訂單表,以訂單id為維度列,訂單的狀態為指標列,且聚合函數為Replace,也就是當訂單id相同時,新訂單狀態覆蓋舊的訂單狀態。
-
這時候版本0~30中訂單1的狀態可能是待付款,版本31~35中訂單1的狀態是已收貨,最后一個版本36中訂單1的狀態是已完成。 那么用戶在查詢每個訂單狀態的時候,不同版本之間的數據就需要進行一個Replace, 取最后一個版本中的已完成狀態作為查詢的結果。
Uniq 模型(唯一主鍵)
在某些多維分析場景下,用戶更關注的是如何保證 Key 的唯一性,即如何獲得 Primary Key 唯一性約束。因此,我們引入了 Uniq 的數據模型。該模型本質上是聚合模型的一個特例,也是一種簡化的表結構表示方式。舉例說明:
這是一個典型的用戶基礎信息表。這類數據沒有聚合需求,只需保證主鍵唯一性。(這里的主鍵為 user_id + username),建表語法如下:
1 CREATE TABLE IF NOT EXISTS example_db.expamle_tbl 2 ( 3 `user_id` LARGEINT NOT NULL COMMENT "用戶id", 4 `username` VARCHAR(50) NOT NULL COMMENT "用戶昵稱", 5 `city` VARCHAR(20) COMMENT "用戶所在城市", 6 `age` SMALLINT COMMENT "用戶年齡", 7 `sex` TINYINT COMMENT "用戶性別", 8 `phone` LARGEINT COMMENT "用戶電話", 9 `address` VARCHAR(500) COMMENT "用戶地址", 10 `register_time` DATETIME COMMENT "用戶注冊時間" 11 ) 12 UNIQUE KEY(`user_id`, `user_name`) 13 ... /* 省略 Partition 和 Distribution 信息 */
而這個表結構,完全同等於以下使用聚合模型描述的表結構:
即 Uniq 模型完全可以用聚合模型中的 REPLACE 方式替代。其內部的實現方式和數據存儲方式也完全一樣
Duplicate (明細模型)
在某些多維分析場景下,數據既沒有主鍵,也沒有聚合需求。因此,我們引入 Duplicate 數據模型來滿足這類需求。舉例說明
建表語句:
1 CREATE TABLE IF NOT EXISTS example_db.expamle_tbl 2 ( 3 `timestamp` DATETIME NOT NULL COMMENT "日志時間", 4 `type` INT NOT NULL COMMENT "日志類型", 5 `error_code` INT COMMENT "錯誤碼", 6 `error_msg` VARCHAR(1024) COMMENT "錯誤詳細信息", 7 `op_id` BIGINT COMMENT "負責人id", 8 `op_time` DATETIME COMMENT "處理時間" 9 ) 10 DUPLICATE KEY(`timestamp`, `type`) 11 ... /* 省略 Partition 和 Distribution 信息 */
這種數據模型區別於 Aggregate 和 Uniq 模型。數據完全按照導入文件中的數據進行存儲,不會有任何聚合。即使兩行數據完全相同,也都會保留。 而在建表語句中指定的 DUPLICATE KEY,只是用來指明底層數據按照那些列進行排序。(更貼切的名稱應該為 “Sorted Column”,這里取名 “DUPLICATE KEY” 只是用以明確表示所用的數據模型 )
明細模型就像Mysql中的表一樣,優勢就在於你可以詳細追溯每個用戶行為或訂單詳情。但劣勢也很明顯,分析型的查詢效率不高。
構建Rollup:
ROLLUP 在多維分析中是“上卷”的意思,也就是從細粒度的數據向高層的聚合。
在 Doris 中,我們將用戶通過建表語句創建出來的表成為 Base 表(Base Table),Base 表中保存着按用戶建表語句指定的方式存儲的基礎數據。在 Base 表之上,我們可以創建任意多個 ROLLUP 表。這些 ROLLUP 的數據是基於 Base 表產生的,並且在物理上是獨立存儲的。
Rollup還有一點好處在於,由於Doris具有在原始數據上實時計算的能力,因此不需要對所有維度的每個組合都創建Rollup。尤其是在維度很多的情況下,可以取得一個存儲空間和查詢效率之間的平衡。
舉例:Aggregate 和 Uniq 模型中的 ROLLUP, 因為 Uniq 只是 Aggregate 模型的一個特例,所以這里我們不加以區別
例如: 在剛才的廣告點擊率表上選取 城市 和 廣告id 作為維度列構建一個Rollup表。
- 那么不同時間的相同 廣告id ,就會再次進行聚合。比如12月31日和1月1日在美國對廣告1的點擊量就會相加得到40這個總和。
- 在導入新數據的過程中:為保證Rollup表和原始表的數據一致性。新增數據會同時導入Base表和Rollup表。對Rollup表的更新並不是拿整個Base數據重新構建一遍Rollup,而是增量更新。
- 例如: 12月31日廣告1在美國的點擊量增加一次,則Base表中的點擊量就會變成11,而Rollup表中的點擊量就會變成41。
- 查詢的時候會根據查詢需要的維度列以及聚合方式,自動匹配到最優的(一般就是聚合程度最高的)Rollup表。由於聚合數據已經提前計算好了,所以Rollup是一個能加快查詢效率的功能。
- 例如: 如果想要分析每條廣告在不同國家的點擊量,Doris就會自動命中這個 ROLLUP 表,從而只需掃描極少的數據量,即可完成這次聚合查詢。
Duplicate 模型中的 ROLLUP
因為 Duplicate 模型沒有聚合的語意。所以該模型中的 ROLLUP,已經失去了“上卷”這一層含義。而僅僅是作為調整列順序,以命中前綴索引的作用。
前綴索引與 ROLLUP
不同於傳統的數據庫設計,Doris 不支持在任意列上創建索引。Doris 這類 MPP 架構的 OLAP 數據庫,通常都是通過提高並發,來處理大量數據的。
本質上,Doris 的數據存儲在類似 SSTable(Sorted String Table)的數據結構中。該結構是一種有序的數據結構,可以按照指定的列進行排序存儲。在這種數據結構上,以排序列作為條件進行查找,會非常的高效。
在 Aggregate、Uniq 和 Duplicate 三種數據模型中。底層的數據存儲,是按照各自建表語句中,AGGREGATE KEY、UNIQ KEY 和 DUPLICATE KEY 中指定的列進行排序存儲的。
而前綴索引,即在排序的基礎上,實現的一種根據給定前綴列,快速查詢數據的索引方式。
因為建表時已經指定了列順序,所以一個表只有一種前綴索引。這對於使用其他不能命中前綴索引的列作為條件進行的查詢來說,效率上可能無法滿足需求。因此,我們可以通過創建 ROLLUP 來人為的調整列順序。舉例說明
Base 表結構如下:
我們可以在此基礎上創建一個 ROLLUP 表:
可以看到,ROLLUP 和 Base 表的列完全一樣,只是將 user_id 和 age 的順序調換了。那么當我們進行如下查詢時:
- SELECT * FROM table where age=20 and massage LIKE "%error%";
會優先選擇 ROLLUP 表,因為 ROLLUP 的前綴索引匹配度更高。
數據模型小結:
1、聚合模型特點與適合場景:
- 老數據不會被更新,只會追加新數據
- 不需要召回原始的明細數據
- 業務方進行的查詢為匯總類查詢
- 場景如:網站或App的訪問流量、廣告點擊總量等
2、唯一主鍵模型特點與適合場景:
- 已經寫入的數據有大量的更新需求;
- 注意事項:
- 導入數據時需要將所有字段補全才能夠完成更新操作
- 對於更新模型的數據讀取,需要在查詢時完成多版本合並,當版本過多時會導致查詢性能降低。所以在向更新模型導入數據時,應該適當降低導入頻率,從而提升查詢性能。
- 因為合並過程需要將所有主鍵字段進行比較,所以應該避免放置過多的主鍵字段,以免降低查詢性能。
3、明細模型特點與適合場景:
- 明細模型表中導入完全相同的兩行數據時,DorisDB會認為是兩行數據
- 需要保留原始的數據(例如原始日志,原始操作記錄等)來進行分析的場景
- 注意:
- 充分利用排序列,在建表時將經常在查詢中用於過濾的列放在表的前面,這樣能夠提升查詢速度。
- 明細模型中, 可以指定部分的維度列為排序鍵; 而聚合模型和更新模型中, 排序鍵只能是全體維度列
4、數據模型的選擇建議
- 因為數據模型在建表時就已經確定,且無法修改。所以,選擇一個合適的數據模型非常重要
- Aggregate 模型可以通過預聚合,極大地降低聚合查詢時所需掃描的數據量和查詢的計算量,非常適合有固定模式的報表類查詢場景。但是該模型對 count(*) 查詢很不友好。同時因為固定了 Value 列上的聚合方式,在進行其他類型的聚合查詢時,需要考慮語意正確性。
- Uniq 模型針對需要唯一主鍵約束的場景,可以保證主鍵唯一性約束。但是無法利用 ROLLUP 等預聚合帶來的查詢優勢(因為本質是 REPLACE,沒有 SUM 這種聚合方式)
- Duplicate 適合任意維度的 Ad-hoc 查詢。雖然同樣無法利用預聚合的特性,但是不受聚合模型的約束,可以發揮列存模型的優勢(只讀取相關列,而不需要讀取所有 Key 列)
5、ROLLUP 小結:
- ROLLUP 最根本的作用是提高某些查詢的查詢效率(無論是通過聚合來減少數據量,還是修改列順序以匹配前綴索引)。因此 ROLLUP 的含義已經超出了 “上卷” 的范圍。這也是為什么我們在源代碼中,將其命名為 Materized Index(物化索引)的原因。
- ROLLUP 是附屬於 Base 表的,可以看做是 Base 表的一種輔助數據結構。用戶可以在 Base 表的基礎上,創建或刪除 ROLLUP,但是不能在查詢中顯式的指定查詢某 ROLLUP。是否命中 ROLLUP 完全由 Doris 系統自動決定。
- ROLLUP 的數據是獨立物理存儲的。因此,創建的 ROLLUP 越多,占用的磁盤空間也就越大。同時對導入速度也會有影響(導入的ETL階段會自動產生所有 ROLLUP 的數據),但是不會降低查詢效率(只會更好)。
- ROLLUP 的數據更新與 Base 表示完全同步的。
- ROLLUP 中列的聚合方式,與 Base 表完全相同。在創建 ROLLUP 無需指定,也不能修改。
- 查詢能否命中 ROLLUP 的一個必要條件(非充分條件)是,查詢所涉及的所有列(包括 select list 和 where 中的查詢條件列等)都存在於該 ROLLUP 的列中。否則,查詢只能命中 Base 表。
- 某些類型的查詢(如 count(*))在任何條件下,都無法命中 ROLLUP
- 可以通過 EXPLAIN your_sql; 命令獲得查詢執行計划,在執行計划中,查看是否命中 ROLLUP
- 可以通過 DESC tbl_name ALL; 語句顯示 Base 表和所有已創建完成的 ROLLUP
- 這里Rollup說明的補充Rollup
Flag:
- 比較適合於Once Write 的場景
- 交易訂單存在多次更新的場景,同時也存在着按維度聚合的場景,好象並不適合此組件?
參考資料
- https://blog.csdn.net/kaede1209/article/details/112243447
- https://ai.baidu.com/forum/topic/show/987485
- https://www.kancloud.cn/dorisdb/dorisdb/2142134
- https://www.yuque.com/zailushang-j5dyp/wn32ln/rgf09k
- Apache Doris (Incubating) 原理與實踐
- https://ai.baidu.com/forum/topic/show/987485
- Doris核心功能介紹—預聚合引擎和物化視圖