開始文章前先扯個淡
最近在思考統計功能的時候,一開始是尋思著用程式配合mysql來進行儲存,但是如果用mysql來存一些比較細節話的資料然後再做統計,資料量會非常龐大,然後又思考用mysql儲存資料,然後用定時指令碼或佇列來按照具體要求更新到redis中,這樣查詢就很快了。
後來想起我們本身用的plausible
來記錄資料,然後尋思著用plausible
的event
來記錄自定義的goals
,發現的確可行,模擬了500萬資料之後,表現良好,感覺自己棒棒噠。誰知道資料量起來之後,2000萬的資料就能讓服務炸了。期間,也遇到了一些其他問題,也給官方提了一些建議,逼不得已不得不學了elixir
,當然這是後話了。這裡提一嘴這個plausible
主要是給後來者提個醒,如果也想用plausible
來做自定義的一些統計功能,最好是放棄掉,改為直接用ClickHouse
來處理。
最後沒有辦法,我觀察了plausible
的資料儲存結構,發現根本原因就是event
的儲存型別導致的,效能誤解,從而萌生了去研究一下ClickHouse
,看看自己實現能不能得到很好的效能,於是便有了這篇文章,如果對大家有用,歡迎探討,如果理解有錯的話,歡迎各位給我斧正。
行式資料庫 vs 列式資料庫結構對比
以下先以官方文件的表格來拋磚引玉,記錄一下這段時間對於clickhouse的學習成果。
行式資料庫
Row | WatchID | JavaEnable | Title | GoodEvent | EventTime |
---|---|---|---|---|---|
#0 | 89354350662 | 1 | Investor Relations | 1 | 2016-05-18 05:19:20 |
#1 | 90329509958 | 0 | Contact us | 1 | 2016-05-18 08:10:20 |
#2 | 89953706054 | 1 | Mission | 1 | 2016-05-18 07:38:00 |
#N | … | … | … | … | … |
列式資料庫
Row: | #0 | #1 | #2 | #N |
---|---|---|---|---|
WatchID: | 89354350662 | 90329509958 | 89953706054 | … |
JavaEnable: | 1 | 0 | 1 | … |
Title: | Investor Relations | Contact us | Mission | … |
GoodEvent: | 1 | 1 | 1 | … |
EventTime: | 2016-05-18 05:19:20 | 2016-05-18 08:10:20 | 2016-05-18 07:38:00 | … |
從表格上看,一下子比較難以理解,行式資料庫和我們電子表格看到的表現一樣,但是儲存是一行一行的儲存,列式資料庫是以一列一列的儲存。以下是以檔案儲存結構來做一個示例:
也就是說,對於行資料庫,一行資料是形成一個完整的檔案(實際底層具體儲存不是這樣子,這裡是方便說明整個思想),這個檔案包含了這一整行的每個資料欄位,取到這一行就能拿到完整的資料。
而對於列式儲存來說,一列資料為一個檔案,如以上所展示的,WatchId
的資料全部存在WatchId
檔案裡面。
然後我做了個腦圖再次對這個結構做個對比。
以下用json來描述兩種資料結構的區別:
行式資料庫儲存結構
[ { "id": 1, "title": "t1", "content": "c1" }, { "id": 2, "title": "t2", "content": "c2" }, { "id": 2, "title": "t2", "content": "c2" }, { "id": 3, "title": "t3", "content": "c3" } ]
列式資料庫結構
{ "id": [ 1, 2, 3, 4 ], "title": [ "t1", "t2", "t3", "t4" ], "content": [ "c1", "c2", "c3", "c4" ] }
列式資料庫主要應用場景
列式資料庫主要運用場景為OLAP
(聯機分析),也就是說主要應用於資料分析這一塊,通常不用來處理具體的業務資料儲存。行式資料庫主要用於處理業務資料的儲存,業務資料的特點是對單條資料經常會產生更新,會對資料一致性有非常嚴格的要求。對於OLAP來說,資料很少會有變動,所以通常來說,列式資料庫主要目的是為了更快的寫入以及更快的資料查詢和統計。
以下我列出一些OLAP
場景的關鍵特徵:
- 絕大多數是讀請求
- 資料以相當大的批次(> 1000行)更新,而不是單行更新;或者根本沒有更新。
- 已新增到資料庫的資料不能修改。
- 對於讀取,從資料庫中提取相當多的行,但只提取列的一小部分。
- 寬表,即每個表包含著大量的列
- 查詢相對較少(通常每臺伺服器每秒查詢數百次或更少)
- 對於簡單查詢,允許延遲大約50毫秒
- 列中的資料相對較小:數字和短字串(例如,每個URL 60個位元組)
- 處理單個查詢時需要高吞吐量(每臺伺服器每秒可達數十億行)
- 事務不是必須的
- 對資料一致性要求低
- 每個查詢有一個大表。除了他以外,其他的都很小。
- 查詢結果明顯小於源資料。換句話說,資料經過過濾或聚合,因此結果適合於單個伺服器的RAM中
MergeTree(合併樹) 資料表引擎
主要特點
MergeTree
是ClickHouse
最基礎的資料儲存引擎,絕大部分時候都是用這個資料引擎。
MergeTree
系列的引擎被設計用於插入極大量的資料到一張表當中。資料可以以資料片段的形式一個接著一個的快速寫入,資料片段在後臺按照一定的規則進行合併。相比在插入時不斷修改(重寫)已儲存的資料,這種策略會高效很多。
主要特點:
儲存的資料按主鍵排序。
這使得您能夠建立一個小型的稀疏索引來加快資料檢索。
如果指定了
分割槽鍵
的話,可以使用分割槽。在相同資料集和相同結果集的情況下 ClickHouse 中某些帶分割槽的操作會比普通操作更快。查詢中指定了分割槽鍵時 ClickHouse 會自動擷取分割槽資料。這也有效增加了查詢效能。
通常來說,都是以時間來進行分割槽,從表象來看的話,就是一個分割槽是一個資料夾,這樣可以避免一個檔案儲存大量的資料。
支援資料副本。
ReplicatedMergeTree
系列的表提供了資料副本功能。支援資料取樣。
需要的話,您可以給表設定一個取樣方法。
結構說明
這段時間為了研究plausible
的統計效能問題,主要也是圍繞業務展開的,對其他幾個資料引擎沒有做深入的瞭解,其他引擎也是繼承於MergeTree
引擎,如果大家感興趣可以瞭解一下,以下我大致列出其他幾個資料引擎:
VersionedCollapsingMergeTree
- 允許快速寫入不斷變化的物件狀態。
- 刪除後臺中的舊物件狀態。 這顯著降低了儲存體積。
GraphiteMergeTree
- 該引擎用來對 Graphite資料進行瘦身及彙總。對於想使用CH來儲存Graphite資料的開發者來說可能有用。
- 如果不需要對Graphite資料做彙總,那麼可以使用任意的CH表引擎;但若需要,那就採用
GraphiteMergeTree
引擎。它能減少儲存空間,同時能提高Graphite資料的查詢效率。
AggregatingMergeTree
CollapsingMergeTree
- 該引擎繼承於 MergeTree,並在資料塊合併演算法中新增了摺疊行的邏輯。
ReplacingMergeTree
- 該引擎和 MergeTree 的不同之處在於它會刪除排序鍵值相同的重複項
SummingMergeTree
- 當合並
SummingMergeTree
表的資料片段時,ClickHouse 會把所有具有相同主鍵的行合併為一行,該行包含了被合併的行中具有數值資料型別的列的彙總值。如果主鍵的組合方式使得單個鍵值對應於大量的行,則可以顯著的減少儲存空間並加快資料查詢的速度。
- 當合並
ClickHouse 資料庫目前梳理的一些注意點
- 不適合處理事務,沒有完整的事務支援。
- 缺少高頻率,低延遲的修改或刪除已存在資料的能力。僅能用於批量刪除或修改資料,資料更新是非同步的
- 大部分資料查詢適用標準的SQL語句,如果一直使用
MySQL
這種行式資料庫,沒太多切換成本 - 定義排序欄位或者說是定義索引,儲存時候就會按照排序儲存,範圍查詢速度非常快
ClickHouse
提供各種各樣在允許犧牲資料精度的情況下對查詢進行加速的方法(目前我沒有用過,暫時不知道應用場景)- 稀疏索引使得
ClickHouse
不適合通過其鍵檢索單行的點查詢。 - 資料型別豐富,支援
Map(key,value)
,Tuple(T1,T2,...)
,Array(T)
,Geo
,Nested巢狀資料結構(類似於巢狀表)
等 - Datetime 型別
group by
有toStartOf*
開頭的轉換函式,如toStartOfDay
,相比用DATE
函式轉換,能提升幾十倍效能