Doris 一種實時多維分析的解決方案

Doris

Doris 這類 MPP 架構的 OLAP 數據庫,通常都是通過提高併發,來處理大量數據的。本質上,Doris 的數據存儲在類似 SSTable(Sorted String Table)的數據結構中。該結構是一種有序的數據結構,可以按照指定的列進行排序存儲。在這種數據結構上,以排序列作爲條件進行查找,會非常的高效。

限制

  • Count(*) 語法方面,原生的方式性能不是特別高,需要自行優化(http://doris.apache.org/documentation/cn/getting-started/data-model-rollup.html)
  • 不存在除了維度和指標之外的字段類型存在,如果需要實現多種需求場景,需要創建多種表類型來冗餘數據方式實現

數據存儲結構

在 Doris 中,數據以表(Table)的形式進行邏輯上的描述。一張表包括行(Row)和列(Column)。Row 即用戶的一行數據。Column 用於描述一行數據中不同的字段。

Column 可以分爲兩大類:Key 和 Value。從業務角度看,Key 和 Value 可以分別對應維度列和指標列。

Doris 的數據模型主要分爲3類:

  • Aggregate
  • Uniq
  • Duplicate

Aggregate 模型

在 Doris 通過 key 來來決定 value 的聚合粒度大小。

CREATE TABLE IF NOT EXISTS example_db.expamle_tbl
(
    `user_id` LARGEINT NOT NULL COMMENT "用戶id",
    `date` DATE NOT NULL COMMENT "數據灌入日期時間",
    `city` VARCHAR(20) COMMENT "用戶所在城市",
    `age` SMALLINT COMMENT "用戶年齡",
    `sex` TINYINT COMMENT "用戶性別",
    `last_visit_date` DATETIME REPLACE DEFAULT "1970-01-01 00:00:00" COMMENT "用戶最後一次訪問時間",
    `cost` BIGINT SUM DEFAULT "0" COMMENT "用戶總消費",
    `max_dwell_time` INT MAX DEFAULT "0" COMMENT "用戶最大停留時間",
    `min_dwell_time` INT MIN DEFAULT "99999" COMMENT "用戶最小停留時間",
)
AGGREGATE KEY(`user_id`, `date`, `timestamp`, `city`, `age`, `sex`)
... /* 省略 Partition 和 Distribution 信息 */
;

像帶有 REPLACE、SUM、MAX、MIN 這種標記的字段都是屬於 value,user_id, date, timestamp, city, age, sex 則爲key。

Uniq模型

這類數據沒有聚合需求,只需保證主鍵唯一性。

CREATE TABLE IF NOT EXISTS example_db.expamle_tbl
(
    `user_id` LARGEINT NOT NULL COMMENT "用戶id",
    `username` VARCHAR(50) NOT NULL COMMENT "用戶暱稱",
    `city` VARCHAR(20) COMMENT "用戶所在城市",
    `age` SMALLINT COMMENT "用戶年齡",
    `sex` TINYINT COMMENT "用戶性別",
    `phone` LARGEINT COMMENT "用戶電話",
    `address` VARCHAR(500) COMMENT "用戶地址",
    `register_time` DATETIME COMMENT "用戶註冊時間"
)
UNIQUE KEY(`user_id`, `user_name`)
... /* 省略 Partition 和 Distribution 信息 */
;

Duplicate 模型

在某些多維分析場景下,數據既沒有主鍵,也沒有聚合需求。因此,我們引入 Duplicate 數據模型來滿足這類需求。

這種數據模型區別於 Aggregate 和 Uniq 模型。數據完全按照導入文件中的數據進行存儲,不會有任何聚合。即使兩行數據完全相同,也都會保留。 而在建表語句中指定的 DUPLICATE KEY,只是用來指明底層數據按照那些列進行排序

在 DUPLICATE KEY 的選擇上,我們建議適當的選擇前 2-4 列就可以。

CREATE TABLE IF NOT EXISTS example_db.expamle_tbl
(
    `timestamp` DATETIME NOT NULL COMMENT "日誌時間",
    `type` INT NOT NULL COMMENT "日誌類型",
    `error_code` INT COMMENT "錯誤碼",
    `error_msg` VARCHAR(1024) COMMENT "錯誤詳細信息",
    `op_id` BIGINT COMMENT "負責人id",
    `op_time` DATETIME COMMENT "處理時間"
)
DUPLICATE KEY(`timestamp`, `type`)
... /* 省略 Partition 和 Distribution 信息 */
;

數據模型的選擇建議

因爲數據模型在建表時就已經確定,且無法修改。所以,選擇一個合適的數據模型非常重要

  1. Aggregate 模型可以通過預聚合,極大地降低聚合查詢時所需掃描的數據量和查詢的計算量,非常適合有固定模式的報表類查詢場景。但是該模型對 count(*) 查詢很不友好。同時因爲固定了 Value 列上的聚合方式,在進行其他類型的聚合查詢時,需要考慮語意正確性。
  2. Uniq 模型針對需要唯一主鍵約束的場景,可以保證主鍵唯一性約束。但是無法利用 ROLLUP 等預聚合帶來的查詢優勢(因爲本質是 REPLACE,沒有 SUM 這種聚合方式)。
  3. Duplicate 適合任意維度的 Ad-hoc 查詢。雖然同樣無法利用預聚合的特性,但是不受聚合模型的約束,可以發揮列存模型的優勢(只讀取相關列,而不需要讀取所有 Key 列)。

前綴索引

在 Aggregate、Uniq 和 Duplicate 三種數據模型中。底層的數據存儲,是按照各自建表語句中,AGGREGATE KEY、UNIQ KEY 和 DUPLICATE KEY 中指定的列進行排序存儲的。

而前綴索引,即在排序的基礎上,實現的一種根據給定前綴列,快速查詢數據的索引方式。

我們將一行數據的前 36 個字節 作爲這行數據的前綴索引。當遇到 VARCHAR 類型時,前綴索引會直接截斷。我們舉例說明:

  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 DATETIME
  1. 以下表結構的前綴索引爲 user_name(20 Bytes)。即使沒有達到 36 個字節,因爲遇到 VARCHAR,所以直接截斷,不再往後繼續。
ColumnName 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;

所以在建表時,正確的選擇列順序,能夠極大地提高查詢效率

物化視圖(rollup)

ROLLUP 在多維分析中是“上卷”的意思,即將數據按某種指定的粒度進行進一步聚合。

在 Doris 中,我們將用戶通過建表語句創建出來的表成爲 Base 表(Base Table)。Base 表中保存着按用戶建表語句指定的方式存儲的基礎數據。

在 Base 表之上,我們可以創建任意多個 ROLLUP 表。這些 ROLLUP 的數據是基於 Base 表產生的,並且在物理上是獨立存儲的。

ROLLUP 表的基本作用,在於在 Base 表的基礎上,獲得更粗粒度的聚合數據

Rollup 本質上可以理解爲原始表(Base Table)的一個物化索引。建立 Rollup 時可只選取 Base Table 中的部分列作爲 Schema。Schema 中的字段順序也可與 Base Table 不同。

ROLLUP 創建完成之後的觸發是程序自動的,不需要任何其他指定或者配置。

例如:創建了 user_id (key),cost(value)格式的 rollup 時,當執行下方語句時,就會觸發。

SELECT user_id, sum(cost) FROM table GROUP BY user_id;

Aggregate 和 Uniq 兩種數據存儲格式時,使用 rollup 會改變聚合數據的粒度,但對於 Duplicate 只是調整前綴索引。

因爲建表時已經指定了列順序,所以一個表只有一種前綴索引。這對於使用其他不能命中前綴索引的列作爲條件進行的查詢來說,效率上可能無法滿足需求。因此,我們可以通過創建 ROLLUP 來人爲的調整列順序。舉例說明。

Base 表結構如下:

ColumnName Type
user_id BIGINT
age INT
message VARCHAR(100)
max_dwell_time DATETIME
min_dwell_time DATETIME

我們可以在此基礎上創建一個 ROLLUP 表:

ColumnName Type
age INT
user_id BIGINT
message VARCHAR(100)
max_dwell_time DATETIME
min_dwell_time DATETIME

可以看到,ROLLUP 和 Base 表的列完全一樣,只是將 user_id 和 age 的順序調換了。那麼當我們進行如下查詢時:

SELECT * FROM table where age=20 and massage LIKE "%error%";

會優先選擇 ROLLUP 表,因爲 ROLLUP 的前綴索引匹配度更高。

創建 rollup 語法

ALTER TABLE table1 ADD ROLLUP rollup_city(citycode, pv);
# 取消正在執行的作業
CANCEL ALTER TABLE ROLLUP FROM table1;

ROLLUP 調整前綴索引

因爲建表時已經指定了列順序,所以一個表只有一種前綴索引。這對於使用其他不能命中前綴索引的列作爲條件進行的查詢來說,效率上可能無法滿足需求。因此,我們可以通過創建 ROLLUP 來人爲的調整列順序。

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 數量沒有限制,但數量越多會消耗比較多的內存。支持 SQL 方式變更 rollup 字段數量。

分區和分桶

Doris 支持兩級分區存儲, 第一層爲 RANGE 分區(partition), 第二層爲 HASH 分桶(bucket)。

1.3.1. RANGE分區(partition)

RANGE分區用於將數據劃分成不同區間, 邏輯上可以理解爲將原始表劃分成了多個子表。業務上,多數用戶會選擇採用按時間進行partition, 讓時間進行partition有以下好處:

* 可區分冷熱數據
* 可用上Doris分級存儲(SSD + SATA)的功能
* 按分區刪除數據時,更加迅速

1.3.2. HASH分桶(bucket)

根據hash值將數據劃分成不同的 bucket。

* 建議採用區分度大的列做分桶, 避免出現數據傾斜
* 爲方便數據恢復, 建議單個 bucket 的 size 不要太大, 保持在 10GB 以內, 所以建表或增加 partition 時請合理考慮 bucket 數目, 其中不同 partition 可指定不同的 buckets 數。

稀疏索引和 Bloom Filter

Doris對數據進行有序存儲, 在數據有序的基礎上爲其建立稀疏索引,索引粒度爲 block(1024行)。

稀疏索引選取 schema 中固定長度的前綴作爲索引內容, 目前 Doris 選取 36 個字節的前綴作爲索引。

  • 建表時建議將查詢中常見的過濾字段放在 Schema 的前面, 區分度越大,頻次越高的查詢字段越往前放。
  • 這其中有一個特殊的地方,就是 varchar 類型的字段。varchar 類型字段只能作爲稀疏索引的最後一個字段。索引會在 varchar 處截斷, 因此 varchar 如果出現在前面,可能索引的長度可能不足 36 個字節。具體可以參閱 數據模型、ROLLUP 及前綴索引
  • 除稀疏索引之外, Doris還提供bloomfilter索引, bloomfilter索引對區分度比較大的列過濾效果明顯。 如果考慮到varchar不能放在稀疏索引中, 可以建立bloomfilter索引。

Broadcast/Shuffle Join

系統默認實現 Join 的方式,是將小表進行條件過濾後,將其廣播到大表所在的各個節點上,形成一個內存 Hash 表,然後流式讀出大表的數據進行Hash Join。但是如果當小表過濾後的數據量無法放入內存的話,此時 Join 將無法完成,通常的報錯應該是首先造成內存超限。

如果遇到上述情況,建議使用 Shuffle Join 的方式,也被稱作 Partitioned Join。即將小表和大表都按照 Join 的 key 進行 Hash,然後進行分佈式的 Join。這個對內存的消耗就會分攤到集羣的所有計算節點上

問題

  1. 在已經創建的表基礎上進行表結構字段的變更和 rollup 索引的變更?

支持,但數據模式一旦表創建就無法變更。

  1. rollup 是否存在數量的限制?

不存在,但越多的 rollup 內存資源會消耗更多,同時,導入數據會比較慢。

  1. (A,B,C)構成的索引是否支持僅 A 字段作爲查詢條件查詢?

支持,但要有順序要求。

總結

Doris 表結構由 key 和 value 構成,key 爲維度,value 爲統計指標。適合做簡單的聚合計算和維度計算,使用比較低的硬件條件擁有比較高的性能。

  • 查詢:滿足 MySQL 語法
  • 提升查詢性能:使用前綴索引+rollup 或者使用 partition、bloom 過濾器。
  • 提升 join 方式查詢性能:Shuffle Join。
  • 表結構和索引都支持變更,但數據模式不支持變更。

Doris 官方還推出了 Docker 的 Dev 版本進行特性試用。https://hub.docker.com/r/apachedoris/doris-dev

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章