背景介紹
在交互式分析場景下,很多時候除了固定字段之外,還會有一些動態字段的需求。比如,在遊戲場景下,需要動態存儲用戶每個遊戲的play時長。
這種場景下,我們希望在一張表中同時存儲固定字段和動態字段的信息,並且可以高效地使用動態字段做過濾查詢。
Map類型使用示例
CREATETABLEuser_game_play
(
mid UInt64,
buvidString,
game_play_durationMap(String, UInt32),
log_dateString
)
ENGINE=MergeTree()
PARTITION BY log_date
ORDERBYmid;
insert into user_game_play values (1, '123', map('王者榮耀',3600, 'FGO', 1800), '2021-11-14');
SELECT game_play_duration['王者榮耀'] AS duration FROM test.user_game_play
┌─duration─┐
│ 3600 │
└──────────┘
Map類型的取值實現
ClickHouse從v21.1.2.15-stable版本開始支持Map類型(詳見PR#1586),其讀取key對應value的實現邏輯大致如下:
1. 內部用兩個數組(ColumnArray)分別存儲key和value值,我們分別稱之爲 key_array和value_array。
2. 對於Map的取值操作(即map[‘key’]操作),先在從key_array中找到要找的key的下標,然後根據這個下標到value_array裏獲取對應的值。
具體實現細節詳見源碼FunctionArrayElement::executeMap.
從上述分析可知,Map類型的工作方式本質上和用兩個數組分別存儲key和value的方式是一樣的。只是在功能上做了封裝,提高了用戶使用的便捷性,但在性能上並沒有變化。
Map類型的跳數索引
索引類型
爲了提升map操作的性能,我們在社區版本的Map類型基礎上,給其加上了多種類型的skipping
index,包括 bloom_filter ,tokenbf_v1,ngrambf_v1
上面這三種skipping index本質上都是用bloom filter存儲每個索引粒度的索引值。其中,tokenbf_v1和ngrambf_v1只支持String類型,bloom_filter可支持各種類型。
1. ngrambf_v1是對字符串中固定長度的substring做bloom filter存儲和檢索。
2. tokenbf_v1是對由非字母數字符號分隔開的token做bloom filter存儲和檢索。
3. bloom_filter則是直接對字段取值做bloom filter存儲和檢索。
Map類型的跳數邏輯
在數據寫入到Map類型字段時,所有的key會被抽取出來生成每個索引粒度對應的bloom filter。
對於針對Map類型字段的過濾條件,如:
where game_play_duration[‘王者榮耀’] >= 1800 and game_play_duration[‘王者榮耀’] <=3600
會做以下處理:
1. 從filtering condition中提取map的key。
2. 分析過濾操作符(如 = , >=, <=, >, <, like , in , not in),如果該過濾條件在map不包含對應key時不可能成立,則利用bloom filter過濾掉不可能包含對應key的數據塊(索引粒度)。
具體實現細節詳見源碼PR #28634。
添加索引示例
CREATETABLEuser_game_play
(
mid UInt64,
buvidString,
game_play_durationMap(String, UInt32),
log_dateString,
Index idx game_play_duration TYPE bloom_filter GRANULARITY 2,
)
ENGINE=MergeTree()
PARTITION BY log_date
ORDERBY mid;
影響跳數效果的因素
在我們的性能測試中,給Map類型添加skipping index可以收穫的性能提升差異很大。
效果好的case可以十幾到幾十倍的性能提升,而效果不好的則沒有明顯提升。
跳數索引的過濾效果和兩個數據特性相關:
1. 索引值的cardinality:這個比較好理解,當索引值cardinality很小(比如性別,可取值只有男和女),那麼過濾效果通常有限。
2. 索引值的分佈是否聚集:ClickHouse的跳數索引和主鍵索引一樣,也是稀疏索引。當索引值分佈非常離散時,即使包含查詢值的記錄佔比很小,但可能每個數據塊(索引粒度)都包含查詢值,那麼所有數據都需要讀進內存做過濾判斷。
Map相關函數
ClickHouse社區版本中已經實現了一些map類型相關的函數,包括:
1. map : 基於傳入的鍵值對生成Map類型對象。
2. mapKeys : 獲取map對象的所有keys。
3. mapValues : 獲取map對象的所有values
4. mapContains : 檢查map對象是否包含指定的key。
更多map相關函數細節詳見這裏。
另外,我們添加了兩個map函數mapContainsKeyLike和mapExtractKeyLike(已合併進社區版本,詳見這裏) 。其中mapContainsKeyLike函數支持通過tokenbf_v1索引進行跳數過濾。