我們喜歡ClickHouse的物化視圖. 物化視圖可以實現聚合計算, 從Kafka讀取數據, 實現最后點查詢(last point queries)以及重組表主鍵索引和排序順序. 除了這些功能之外, 物化視圖可以在大量節點上很好地擴縮, 並可以處理大型數據集. 它們是ClickHouse的獨特功能之一.
在計算領域, 強大的功能至少意味着一點點復雜性. 這篇由兩部分組成的文章通過解釋物化視圖的工作原理來填補空白, 從而使初學者也可以有效地使用它們. 我們將通過幾個詳細的示例, 您可以根據自己的使用進行調整. 在此過程中, 我們將探索用於創建視圖的語法的確切含義, 並讓您深入了解ClickHouse在做什么. 示例是完全自包含的, 因此您可以將它們復制/粘貼到clickhouse-client中並自己運行它們.
How Materialized Views Work: Computing Sums
ClickHouse物化視圖自動在表之間轉換數據. 它們類似於觸發器, 對插入的行運行查詢並將結果存入第二個表(每一個insert操作結束后,進行觸發,對每一個insert數據進行查詢插入物化視圖). 讓我們看一個基本的例子. 假設我們有一個記錄用戶下載的表, 如下所示.
1 2 3 4 5 6 7
|
CREATE TABLE download ( when DateTime, userid UInt32, bytes Float32 ) ENGINE=MergeTree PARTITION BY toYYYYMM(when) ORDER BY (userid, when)
|
我們希望跟蹤每個用戶的每日下載. 讓我們看看如何用一個查詢來做到這一點. 首先, 我們需要為單個用戶向表中添加一些數據.
1 2 3 4 5 6 7
|
INSERT INTO download SELECT now() + number * 60 as when, 25, rand() % 100000000 FROM system.numbers LIMIT 5000
|
接下來, 讓我們運行一個查詢來顯示該用戶的每日下載. 當添加新用戶時, 這也將正常工作.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
SELECT toStartOfDay(when) AS day, userid, count() AS downloads, sum(bytes) AS bytes FROM download GROUP BY userid, day ORDER BY userid ASC, day ASC
┌─────────────────day─┬─userid─┬─downloads─┬───────bytes─┐ │ 2020-08-18 00:00:00 │ 25 │ 736 │ 36722522631 │ │ 2020-08-19 00:00:00 │ 25 │ 1440 │ 73305428060 │ │ 2020-08-20 00:00:00 │ 25 │ 1440 │ 73183910537 │ │ 2020-08-21 00:00:00 │ 25 │ 1384 │ 70059352697 │ └─────────────────────┴────────┴───────────┴─────────────┘
|
我們可以通過每次運行查詢以交互方式為應用程序計算這些每日總數, 但是對於大型表, 提前計算它們將更快, 更節省資源. 因此, 最好將結果放在單獨的表格中, 該表格可以連續跟蹤每天每個用戶的下載總數. 我們可以使用以下物化視圖來做到這一點.
1 2 3 4 5 6 7 8 9 10 11
|
CREATE MATERIALIZED VIEW download_daily_mv ENGINE = SummingMergeTree PARTITION BY toYYYYMM(day) ORDER BY (userid, day) POPULATE AS SELECT toStartOfDay(when) AS day, userid, count() as downloads, sum(bytes) AS bytes FROM download GROUP BY userid, day
|
這里有三件重要的事情需要注意. 首先, 物化視圖定義允許類似於CREATE TABLE的語法, 這是有意義的, 因為這個命令將實際創建一個隱藏的目標表(.inner表)來保存視圖數據. 我們使用的ClickHouse引擎旨在使計算和計數變得簡單:SummingMergeTree. 它是用於計算聚合的物化視圖的推薦引擎.
其次, 視圖定義包含關鍵字POPULATE. 這告訴ClickHouse將download
表中的現有數據插入物化視圖. 我們稍后會更多地討論automatic population.
需要注意, 在POPULATE
填充歷史數據的期間, 新進入的這部分數據會被忽略掉, 所以如果對准確性要求非常高, 應慎用
第三, 視圖定義包含一個SELECT語句, 該語句定義在加載視圖時如何轉換數據. 這個查詢在表中的新數據上運行, 以計算每天每個用戶id的下載數量和總字節數. 它本質上與我們以交互方式運行的查詢相同, 只是在本例中, 結果將放在隱藏的目標表(.inner表)中. 我們可以跳過排序, 因為視圖定義已經確保了排序順序.
現在, 我們從物化視圖中查詢數據
1 2 3 4 5 6 7 8 9 10
|
SELECT * FROM download_daily_mv ORDER BY day, userid LIMIT 5
┌─────────────────day─┬─userid─┬─downloads─┬───────bytes─┐ │ 2020-08-18 00:00:00 │ 25 │ 736 │ 36722522631 │ │ 2020-08-19 00:00:00 │ 25 │ 1440 │ 73305428060 │ │ 2020-08-20 00:00:00 │ 25 │ 1440 │ 73183910537 │ │ 2020-08-21 00:00:00 │ 25 │ 1384 │ 70059352697 │ └─────────────────────┴────────┴───────────┴─────────────┘
|
這為我們提供了與先前查詢完全相同的答案. 原因是上面介紹的POPULATE關鍵字. 它確保源表中的現有數據自動加載到視圖中. 不過, 有一個重要警告:如果在填充視圖時插入了新數據, ClickHouse將會丟失它們. 在本系列的第二部分中, 我們將展示如何手動插入數據並避免數據遺漏的問題.
現在, 嘗試使用其他用戶向表中添加更多數據.
1 2 3 4 5 6 7
|
INSERT INTO download SELECT now() + number * 60 as when, 22, rand() % 100000000 FROM system.numbers LIMIT 5000
|
如果您從實例化視圖中進行選擇, 您將看到它現在具有用戶ID 22和25的總數. 請注意, 一旦INSERT完成, 將立即填充新數據. 這是ClickHouse實例化視圖的重要功能, 這使其對於實時分析非常有用.
下面是查詢和新結果.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
SELECT * FROM download_daily_mv ORDER BY userid ASC, day ASC
┌─────────────────day─┬─userid─┬─downloads─┬───────bytes─┐ │ 2020-08-18 00:00:00 │ 22 │ 416 │ 21063519801 │ │ 2020-08-19 00:00:00 │ 22 │ 1440 │ 71523929305 │ │ 2020-08-20 00:00:00 │ 22 │ 1440 │ 70435459582 │ │ 2020-08-21 00:00:00 │ 22 │ 1440 │ 70725673036 │ │ 2020-08-22 00:00:00 │ 22 │ 264 │ 13826466067 │ └─────────────────────┴────────┴───────────┴─────────────┘ ┌─────────────────day─┬─userid─┬─downloads─┬────────bytes─┐ │ 2020-08-18 00:00:00 │ 25 │ 1467 │ 75441118166 │ │ 2020-08-19 00:00:00 │ 25 │ 2880 │ 144811386193 │ │ 2020-08-20 00:00:00 │ 25 │ 2880 │ 145138479865 │ │ 2020-08-21 00:00:00 │ 25 │ 2773 │ 138488485955 │ └─────────────────────┴────────┴───────────┴──────────────┘
|
作為練習, 您可以對源表運行原始查詢, 以確認它與物化視圖中的總數相匹配.
作為最后一個示例, 讓我們使用物化視圖按月匯總. 在本例中, 我們將物化視圖視為一個普通表, 按月分組, 如下所示. 我們添加了WITH TOTALS子句, 它打印一個方便的聚合的總和.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
SELECT toStartOfMonth(day) AS month, userid, sum(downloads), sum(bytes) FROM download_daily_mv GROUP BY userid, month WITH TOTALS ORDER BY userid ASC, month ASC
┌──────month─┬─userid─┬─sum(downloads)─┬───sum(bytes)─┐ │ 2020-08-01 │ 22 │ 5000 │ 247575047791 │ │ 2020-08-01 │ 25 │ 10000 │ 503879470179 │ └────────────┴────────┴────────────────┴──────────────┘
Extremes: ┌──────month─┬─userid─┬─sum(downloads)─┬───sum(bytes)─┐ │ 0000-00-00 │ 0 │ 15000 │ 751454517970 │ └────────────┴────────┴────────────────┴──────────────┘
|
從前面的示例中, 我們可以清楚地看到實例化視圖如何正確地匯總源數據中的數據(how the materialized view correctly summarizes data from the source data). 如上例所示, 我們甚至可以“summarize the summaries”. 那么幕后到底發生了什么? 下圖說明了數據的邏輯流.
logical
如圖所示, 原表上INSERT的值被轉換並應用於隱藏的目標表(.inner表). 要填充視圖(To populate the view), 您要做的就是在源表中插入數據.
您可以從隱藏的目標表(.inner表)和物化視圖中進行查詢. 實際上ClickHouse就是將查詢路由到創建物化視圖時自動創建的internel表中的.
圖中還有另外一件重要的事情需要注意. 物化視圖創建一個具有特殊名稱私有表來保存數據. 如果您通過輸入DROP TABLE download_daily_mv
刪除物化視圖, 則私有表也會被刪除. 如果需要更改視圖, 則需要將其刪除並使用新數據重新創建
1 2 3 4 5 6 7 8
|
>SHOW TABLES
>┌─name─────────────────────┐ >│ .inner.download_daily_mv │ >│ download │ >│ download_daily_mv │ >└──────────────────────────┘
|
.inner.download_daily_mv就是 internel表或叫私有表
Wrap-up - 總結
我們剛剛審閱的示例使用SummingMergeTree創建一個視圖以累加每日用戶下載量. 我們從物化視圖對SELECT使用了標准SQL語法. 這是SummingMergeTree引擎的特殊功能, 僅適用於總和和計數. 對於其他類型的聚合, 我們需要使用其他方法.
另外, 我們的示例使用POPULATE關鍵字將現有表數據發布到視圖創建的私有目標表(.inner表)中. 如果在填充視圖時到達新的INSERT行, ClickHouse將錯過它們. 當您是唯一使用數據集的人時, 此限制很容易解決, 但對於不斷加載數據的生產系統來說是個問題. 此外, 刪除視圖后, 專用表也會消失. 這使得很難更改視圖以適應源表中的架構更改.
在下一篇文章中, 我們將展示如何創建物化視圖來計算其他類型的聚合, 比如平均值或最大值/最小值. 我們還將展示如何顯式定義目標表(.inner表), 並使用我們自己的SQL語句手動將數據加載到其中. 我們還將簡要介紹模式遷移(schema migration). 同時, 我們希望您喜歡這一簡要介紹, 並發現示例有用.
實測及問題
刪除基表, 物化視圖仍然可以查詢
這是符合預期的, 因為物化視圖是存儲了數據的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
bj2-all-clickhouse-test-02 :) drop table download;
DROP TABLE download
Ok.
0 rows in set. Elapsed: 0.016 sec.
bj2-all-clickhouse-test-02 :) show tables;
SHOW TABLES
┌─name─────────────────────┐ │ .inner.download_daily_mv │ │ download_daily_mv │ │ sbtest │ │ sbtest_local │ └──────────────────────────┘
4 rows in set. Elapsed: 0.001 sec.
bj2-all-clickhouse-test-02 :) select * from download_daily_mv;
SELECT * FROM download_daily_mv
┌─────────────────day─┬─userid─┬─downloads─┬────────bytes─┐ │ 2020-08-18 00:00:00 │ 25 │ 1467 │ 75441118166 │ │ 2020-08-19 00:00:00 │ 25 │ 2880 │ 144811386193 │ │ 2020-08-20 00:00:00 │ 25 │ 2880 │ 145138479865 │ │ 2020-08-21 00:00:00 │ 25 │ 2773 │ 138488485955 │ └─────────────────────┴────────┴───────────┴──────────────┘ ┌─────────────────day─┬─userid─┬─downloads─┬───────bytes─┐ │ 2020-08-18 00:00:00 │ 22 │ 416 │ 21063519801 │ │ 2020-08-19 00:00:00 │ 22 │ 1440 │ 71523929305 │ │ 2020-08-20 00:00:00 │ 22 │ 1440 │ 70435459582 │ │ 2020-08-21 00:00:00 │ 22 │ 1440 │ 70725673036 │ │ 2020-08-22 00:00:00 │ 22 │ 264 │ 13826466067 │ └─────────────────────┴────────┴───────────┴─────────────┘
9 rows in set. Elapsed: 0.008 sec.
|
數據在分區合並時聚合
重新創建download表和物化視圖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
SELECT toStartOfDay(when) AS day, userid, count() AS downloads, sum(bytes) AS bytes FROM download GROUP BY userid, day ORDER BY userid ASC, day ASC
┌─────────────────day─┬─userid─┬─downloads─┬───────bytes─┐ │ 2020-08-18 00:00:00 │ 25 │ 387 │ 19855551536 │ │ 2020-08-19 00:00:00 │ 25 │ 1440 │ 73316885071 │ │ 2020-08-20 00:00:00 │ 25 │ 1440 │ 72322018002 │ │ 2020-08-21 00:00:00 │ 25 │ 1440 │ 71675677053 │ │ 2020-08-22 00:00:00 │ 25 │ 293 │ 14125612942 │ └─────────────────────┴────────┴───────────┴─────────────┘
SELECT * FROM download_daily_mv
┌─────────────────day─┬─userid─┬─downloads─┬───────bytes─┐ │ 2020-08-18 00:00:00 │ 25 │ 387 │ 19855551536 │ │ 2020-08-19 00:00:00 │ 25 │ 1440 │ 73316885071 │ │ 2020-08-20 00:00:00 │ 25 │ 1440 │ 72322018002 │ │ 2020-08-21 00:00:00 │ 25 │ 1440 │ 71675677053 │ │ 2020-08-22 00:00:00 │ 25 │ 293 │ 14125612942 │ └─────────────────────┴────────┴───────────┴─────────────┘
|
再次向基表中插入數據, 然后查看物化視圖中的數據
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
INSERT INTO download SELECT now() + (number * 60) AS when, 25, rand() % 100000000 FROM system.numbers LIMIT 5000
SELECT * FROM download_daily_mv
┌─────────────────day─┬─userid─┬─downloads─┬───────bytes─┐ │ 2020-08-18 00:00:00 │ 25 │ 387 │ 19855551536 │ │ 2020-08-19 00:00:00 │ 25 │ 1440 │ 73316885071 │ │ 2020-08-20 00:00:00 │ 25 │ 1440 │ 72322018002 │ │ 2020-08-21 00:00:00 │ 25 │ 1440 │ 71675677053 │ │ 2020-08-22 00:00:00 │ 25 │ 293 │ 14125612942 │ └─────────────────────┴────────┴───────────┴─────────────┘ ┌─────────────────day─┬─userid─┬─downloads─┬───────bytes─┐ │ 2020-08-18 00:00:00 │ 25 │ 385 │ 18771807859 │ │ 2020-08-19 00:00:00 │ 25 │ 1440 │ 73675739078 │ │ 2020-08-20 00:00:00 │ 25 │ 1440 │ 70555293899 │ │ 2020-08-21 00:00:00 │ 25 │ 1440 │ 70773685728 │ │ 2020-08-22 00:00:00 │ 25 │ 295 │ 14750186600 │ └─────────────────────┴────────┴───────────┴─────────────┘
|
可以看到物化視圖中的數據並沒有”全部聚合完整”
查看物化視圖private表分區情況
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
|
bj2-all-clickhouse-test-02 :) select * from system.parts where table='.inner.download_daily_mv'\G
SELECT * FROM system.parts WHERE table = '.inner.download_daily_mv'
Row 1: ────── partition: 202008 name: 202008_1_1_0 part_type: Wide active: 1 marks: 2 rows: 5 bytes_on_disk: 421 data_compressed_bytes: 200 data_uncompressed_bytes: 120 marks_bytes: 192 modification_time: 2020-08-18 17:33:51 remove_time: 0000-00-00 00:00:00 refcount: 1 min_date: 0000-00-00 max_date: 0000-00-00 min_time: 2020-08-18 00:00:00 max_time: 2020-08-22 00:00:00 partition_id: 202008 min_block_number: 1 max_block_number: 1 level: 0 data_version: 1 primary_key_bytes_in_memory: 16 primary_key_bytes_in_memory_allocated: 8192 is_frozen: 0 database: duyalan table: .inner.download_daily_mv engine: SummingMergeTree disk_name: default path: /data/clickhouse/node2/data/duyalan/%2Einner%2Edownload_daily_mv/202008_1_1_0/ hash_of_all_files: f4b55a88dac393d25ffe1c703cca4f6d hash_of_uncompressed_files: 58a4ab29ef36c22884ee7accd528b590 uncompressed_hash_of_compressed_files: e18b006f608580db132ed90adb46902f
Row 2: ────── partition: 202008 name: 202008_2_2_0 part_type: Wide active: 1 marks: 2 rows: 5 bytes_on_disk: 421 data_compressed_bytes: 200 data_uncompressed_bytes: 120 marks_bytes: 192 modification_time: 2020-08-18 17:35:13 remove_time: 0000-00-00 00:00:00 refcount: 1 min_date: 0000-00-00 max_date: 0000-00-00 min_time: 2020-08-18 00:00:00 max_time: 2020-08-22 00:00:00 partition_id: 202008 min_block_number: 2 max_block_number: 2 level: 0 data_version: 2 primary_key_bytes_in_memory: 16 primary_key_bytes_in_memory_allocated: 8192 is_frozen: 0 database: duyalan table: .inner.download_daily_mv engine: SummingMergeTree disk_name: default path: /data/clickhouse/node2/data/duyalan/%2Einner%2Edownload_daily_mv/202008_2_2_0/ hash_of_all_files: 049d090ea65c24be8544ab86846ea9fa hash_of_uncompressed_files: 58a4ab29ef36c22884ee7accd528b590 uncompressed_hash_of_compressed_files: 9ffea93e6b9bc375c7f04374b4a4a6a9
2 rows in set. Elapsed: 0.002 sec.
|
可以看到, 有兩個active分區202008_1_1_0
, 202008_2_2_0
我們手動OPTIMIZE
嘗試合並分區
1 2 3 4 5 6 7
|
bj2-all-clickhouse-test-02 :) optimize table download_daily_mv;
OPTIMIZE TABLE download_daily_mv
Ok.
0 rows in set. Elapsed: 0.004 sec.
|
再次查詢物化視圖數據
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
bj2-all-clickhouse-test-02 :) select * from download_daily_mv;
SELECT * FROM download_daily_mv
┌─────────────────day─┬─userid─┬─downloads─┬────────bytes─┐ │ 2020-08-18 00:00:00 │ 25 │ 772 │ 38627359395 │ │ 2020-08-19 00:00:00 │ 25 │ 2880 │ 146992624149 │ │ 2020-08-20 00:00:00 │ 25 │ 2880 │ 142877311901 │ │ 2020-08-21 00:00:00 │ 25 │ 2880 │ 142449362781 │ │ 2020-08-22 00:00:00 │ 25 │ 588 │ 28875799542 │ └─────────────────────┴────────┴───────────┴──────────────┘
5 rows in set. Elapsed: 0.001 sec.
|
查看private表分區情況
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
|
bj2-all-clickhouse-test-02 :) select * from system.parts where table='.inner.download_daily_mv'\G
SELECT * FROM system.parts WHERE table = '.inner.download_daily_mv'
Row 1: ────── partition: 202008 name: 202008_1_1_0 part_type: Wide active: 0 marks: 2 rows: 5 bytes_on_disk: 421 data_compressed_bytes: 200 data_uncompressed_bytes: 120 marks_bytes: 192 modification_time: 2020-08-18 17:33:51 remove_time: 2020-08-18 17:38:24 refcount: 1 min_date: 0000-00-00 max_date: 0000-00-00 min_time: 2020-08-18 00:00:00 max_time: 2020-08-22 00:00:00 partition_id: 202008 min_block_number: 1 max_block_number: 1 level: 0 data_version: 1 primary_key_bytes_in_memory: 16 primary_key_bytes_in_memory_allocated: 8192 is_frozen: 0 database: duyalan table: .inner.download_daily_mv engine: SummingMergeTree disk_name: default path: /data/clickhouse/node2/data/duyalan/%2Einner%2Edownload_daily_mv/202008_1_1_0/ hash_of_all_files: f4b55a88dac393d25ffe1c703cca4f6d hash_of_uncompressed_files: 58a4ab29ef36c22884ee7accd528b590 uncompressed_hash_of_compressed_files: e18b006f608580db132ed90adb46902f
Row 2: ────── partition: 202008 name: 202008_1_2_1 part_type: Wide active: 1 marks: 2 rows: 5 bytes_on_disk: 421 data_compressed_bytes: 200 data_uncompressed_bytes: 120 marks_bytes: 192 modification_time: 2020-08-18 17:38:24 remove_time: 0000-00-00 00:00:00 refcount: 1 min_date: 0000-00-00 max_date: 0000-00-00 min_time: 2020-08-18 00:00:00 max_time: 2020-08-22 00:00:00 partition_id: 202008 min_block_number: 1 max_block_number: 2 level: 1 data_version: 1 primary_key_bytes_in_memory: 16 primary_key_bytes_in_memory_allocated: 8192 is_frozen: 0 database: duyalan table: .inner.download_daily_mv engine: SummingMergeTree disk_name: default path: /data/clickhouse/node2/data/duyalan/%2Einner%2Edownload_daily_mv/202008_1_2_1/ hash_of_all_files: fdc534a9b9aa0904cde863ee1deff532 hash_of_uncompressed_files: 58a4ab29ef36c22884ee7accd528b590 uncompressed_hash_of_compressed_files: df972eeaad3304d9a16bfa8cb46861ce
Row 3: ────── partition: 202008 name: 202008_2_2_0 part_type: Wide
|