ClickHouse 數據表、數據分區的相關操作,以及 DDL


楔子

在知曉了 ClickHouse 的主要數據類型之后,接下來我們開始介紹 DDL 操作以及定義數據的方法,DDL 查詢提供了數據表的創建、修改和刪除操作,是最常用的功能之一。

數據庫

數據庫起到了命名空間的作用,可以有效規避命名沖突的問題,也為后續的數據隔離提供了支撐。任何一張數據表,都必須歸屬在某個數據庫之下。創建數據庫的完整語法如下所示:

CREATE DATABASE IF NOT EXISTS db_name[ENGINE = engine]

IF NOT EXISTS 表示如果已經存在一個同名的數據庫,則會忽略后續的創建過程;[ENGINE = engine] 表示數據庫所使用的引擎類型(是的,你沒看錯,數據庫也支持設置引擎)。

數據庫目前一共支持 5 種引擎,如下所示。

  • Ordinary:默認引擎,在絕大多數情況下我們都會使用默認引擎,使用時無須刻意聲明,在此數據庫下可以使用任意類型的表引擎
  • Dictionary:字典引擎,此類數據庫會自動為所有數據字典創建它們的數據表,關於數據字典的詳細介紹會在后面展開
  • Memory:內存引擎,用於存放臨時數據。此類數據庫下的數據表只會停留在內存中,不會涉及任何磁盤操作,當服務重啟后數據會被清除
  • Lazy:日志引擎,此類數據庫下只能使用 Log 系列的表引擎,關於 Log 表引擎的詳細介紹會后續章節展開
  • MySQL:MySQL 引擎,此類數據庫下會自動拉取遠端 MySQL 中的數據,並為它們創建 MySQL 表引擎的數據表,關於MySQL表引擎的詳細介紹也會在后續章節展開。

在絕大多數情況下都只需使用默認的數據庫引擎,例如執行下面的語句,便能夠創建屬於我們的第一個數據庫:

CREATE DATABASE kagura_nana;

數據庫的實質就是物理磁盤上的一個目錄文件,所以在語句執行之后,ClickHouse 便會在安裝路徑下創建 kagura_nana 數據庫的目錄文件。

[root@satori ~]# ls /var/lib/clickhouse/data/
default  kagura_nana  system
[root@satori ~]#

與此同時,在 metadata 路徑下也會一同創建用於恢復數據庫的 kagura_nana.sql 文件:

[root@satori ~]# ls /var/lib/clickhouse/metadata/
default  default.sql  kagura_nana  kagura_nana.sql  system  system.sql
[root@satori ~]#

使用 SHOW DATABASES 查詢,能夠返回 ClickHouse 當前的數據庫列表:

satori :) SHOW DATABASES;

SHOW DATABASES

Query id: 2a4de6ad-535f-489f-955e-dae511a18415

┌─name────────┐
│ default     │
│ kagura_nana │
│ system      │
└─────────────┘

3 rows in set. Elapsed: 0.002 sec.

satori :)

使用 USE 查詢可以實現在多個數據庫之間進行切換,而通過 SHOW TABLES 查詢可以查看當前數據庫中存在的所以數據表。刪除一個數據庫,則需要用到下面的 DROP 查詢。

DROP DATABASE [IF EXISTS] db_name;

數據表

我們說數據庫在物理磁盤上對應一個目錄文件,而表則是在數據庫對應的目錄文件里面再創建一個目錄文件,比如我們在數據庫 kagura_nana 里面創建一張表 t,那么相當於在 /var/lib/clickhouse/data/kagura_nana 里面創建一個目錄文件 t,而往表 t 里面寫的數據則會在目錄 t 中以文本文件的形式保存,所以整個邏輯還是比較清晰好理解的。而 ClickHouse 數據表的定義語法,是在標准 SQL 的基礎之上建立的,所以熟悉數據庫的你在看到接下來的語法時,應該會感到很熟悉。ClickHouse 目前提供了三種最基本的建表方法:

第一種是常規定義方法,它的完整語法如下所示:

CREATE TABLE [IF NOT EXISTS] [db_name.]table_name (
    column_name1 type [DEFAULT|MATERIALIZED|ALIAS expr],
    column_name2 type [DEFAULT|MATERIALIZED|ALIAS expr],
    ......
) ENGINE = engine

使用 [db_name.] 參數可以為數據表指定數據庫,如果不指定此參數,則默認會使用 default 數據庫。注意結尾的 ENGINE 參數,它被用於指定數據表的引擎,表引擎決定了數據表的特性,也決定了數據將會被如何存儲以及如何加載。例如 Memory 表引擎,它是 ClickHouse 最簡單的表引擎,數據只會被保存在內存中,在服務重啟時數據會丟失。我們會在后續章節詳細介紹數據表引擎,此處暫不展開。

第二種定義方法是復制其他表的結構,具體語法如下所示:

CREATE TABLE [IF NOT EXISTS] [db_name1.]table_name1 AS [db_name2.]table_name2 [ENGINE = engine]

這種方式支持在不同的數據庫之間復制表結構,例如下面的語句:

-- 將 A 庫下的 a 表拷貝一份到 B 庫下的 b 表, 注意:引擎可以更換
CREATE TABLE IF NOT EXISTS A.a AS B.b ENGINE = TinyLog

第三種定義方法是通過 SELECT 子句的形式創建,它的完整語法如下:

CREATE TABLE [IF NOT EXISTS] [db_name].table_name ENGINE = engine AS SELECT ...

在這種方式下,不僅會根據 SELECT 子句建立相應的表結構,同時還會將 SELECT 子句查詢的數據順帶寫入,例如執行下面的語句:

CREATE TABLE IF NOT EXISTS db.not_exists_table ENGINE = Memory AS SELECT * FROM db.exists_table

上述語句會將 SELECT * FROM db.exists_table 的查詢結果一並寫入數據表。

ClickHouse 和大多數數據庫一樣,使用 DESC 查詢可以返回數據表的定義結構,另外如果想刪除一張數據表,則可以使用下面的 DROP 語句:

DROP TABLE [IF EXISTS] [db_name.]table_name

默認值表達式

表字段支持三種默認值表達式的定義方法,分別是 DEFAULT、MATERIALIZED 和 ALIAS。無論使用哪種形式,表字段一旦定義了默認值,那么便不再強制要求定義數據類型,因為 ClickHouse 會根據默認值進行類型推斷。如果同時對表字段定義了數據類型和默認值表達式,則以明確定義的數據類型為主,例如:

CREATE TABLE table_name (    id String,    col1 DEFAULT 100,    col2 String DEFAULT col1) ENGINE=Memory

col1 字段沒有定義數據類型,默認值為整型 1000;col2 字段定義了數據類型和默認值,且默認值等於 col1,現在寫入測試數據。

INSERT INTO table_name(id) VALUES('AAA');

在寫入之后執行以下查詢:

satori :) SELECT id, col1, col2, toTypeName(col1), toTypeName(col2) from table_nameSELECT    id,    col1,    col2,    toTypeName(col1),    toTypeName(col2)FROM table_nameQuery id: d9114fe3-172f-4e2f-bd2a-13b514015de9┌─id──┬─col1─┬─col2─┬─toTypeName(col1)─┬─toTypeName(col2)─┐│ AAA │  100 │ 100  │ UInt8            │ String           │└─────┴──────┴──────┴──────────────────┴──────────────────┘1 rows in set. Elapsed: 0.002 sec.satori :)

由查詢結果可以驗證,默認值的優先級符合我們的預期,其中 col1 字段根據默認值被推斷為 UInt16;而 col2 字段由於同時定義了數據類型和默認值,所以它最終的數據類型來自明確定義的 String。


默認值表達式的三種定義方法之間也存在着不同之處,可以從如下三個方面進行比較。

1)數據寫入:在數據寫入時,只有 DEFAULT 類型的字段可以出現在 INSERT 語句中,而 MATERIALIZED 和 ALIAS 都不能被顯式賦值,它們只能依靠計算取值。例如試圖為 MATERIALIZED 類型的字段寫入數據,將會得到如下的錯誤。

DB::Exception: Cannot insert column URL,because it is MATERIALIZED column..

2)數據查詢:在數據查詢時,只有 DEFAULT 類型的字段可以通過 SELECT * 返回,而 MATERIALIZED 和 ALIAS 類型的字段不會出現在 SELECT * 查詢的返回結果集中。

3)數據存儲:在數據存儲時,只有 DEFAULT 和 MATERIALIZED 類型的字段才支持持久化。如果使用的表引擎支持物理存儲(例如 TinyLog 表引擎),那么這些列字段將會擁有物理存儲。而 ALIAS 類型的字段不支持持久化,它的取值總是需要依靠計算產生,數據不會落到磁盤。

可以使用 ALTER 語句修改默認值,例如:

ALTER TABLE [db_name.]table_name MODIFY COLOMN col_name DEFAUET value

修改動作並不會影響數據表內先前已經存在的數據,但是默認值的修改有諸多限制,例如在 MergeTree 表引擎中,它的主鍵字段是無法被修改的;而某些表引擎則完全不支持修改(例如 TinyLog)。

臨時表

ClickHouse 也有臨時表的概念,創建臨時表的方法是在普通表的基礎之上添加 TEMPORARY 關鍵字,CREATE TEMPORARY TABLE...,剩余的部分和創建普通表完全一樣。

相比普通表而言,臨時表有如下兩點特殊之處:

  • 它的生命周期是會話綁定的,所以它只支持 Memory 表引擎,如果會話結束,數據表就會被銷毀;
  • 臨時表不屬於任何數據庫,所以在它的建表語句中,既沒有數據庫參數也沒有表引擎參數;

針對第二個特殊項,有人心中難免會產生一個疑問:既然臨時表不屬於任何數據庫,如果臨時表和普通表名稱相同,會出現什么狀況呢?接下來不妨做個測試。首先在 DEFAULT 數據庫創建測試表並寫入數據:

CREATE TABLE tmp_v1(title String) ENGINE = Memory;INSERT INTO tmp_v1 VALUES ('xxx');

接着創建一張名稱相同的臨時表並寫入數據:

CREATE TEMPORARY TABLE tmp_v1(num UInt8) ENGINE = Memory;INSERT INTO tmp_v1 VALUES (22);

現在查詢 tmp_v1 看看會發生什么?

satori :) SELECT * FROM tmp_v1

SELECT *
FROM tmp_v1

Query id: ad4b5094-3627-4af7-a207-bbd8ef90617a

┌─num─┐
│  22 │
└─────┘

1 rows in set. Elapsed: 0.002 sec.

satori :)

通過返回結果可以得出結論,臨時表的優先級是大於普通表的。當兩張數據表名稱相同的時候,會優先讀取臨時表的數據。當然在 ClickHouse 的日常使用中,不會刻意地使用臨時表,它更多被運用在 ClickHouse 的內部,是數據在集群之間傳播的載體。

分區表

數據分區(partition)和數據分片(shard)是完全不同的而個概念,數據分區是針對本地數據而言的,是數據的一種縱向切分;而數據分片是數據的一種橫向切分(后續章節會詳細說)。數據分區對於一款 OLAP 數據庫而言意義非凡,借助數據分區,在后續的查詢過程中能夠跳過不必要的數據目錄,從而提升查詢的性能。合理地利用分區特性,還可以變相實現數據的更新操作,因為數據分區支持刪除、替換和重置操作。假設數據表按照月份分區,那么數據就可以按月份的粒度被替換更新。

分區雖好,但不是所有的表引擎都可以使用這項特性,目前只有合並樹(MergeTree)家族系列的表引擎才支持數據分區。接下來通過一個簡單的例子演示分區表的使用方法,首先由 PARTITION BY 指定分區鍵,例如下面的數據表 partition_v1 使用了日期字段作為分區鍵,並將其格式化為年月的形式:

CREATE TABLE partition_v1 (
    ID String,
    URL String,
    EventDate Date
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(EventDate)
ORDER BY ID

接着寫入不同月份的測試數據:

INSERT INTO partition_v1 VALUES ('a1', 'www.a1.com', '2019-05-01'), ('a2', 'www.a2.com', '2019-06-02')

最后通過 system.parts 系統表,查詢數據表的分區狀態:

SELECT table, partition, path FROM system.parts WHERE table = 'partition_v1'

可以看到,partition_v1 按年月划分后,目前擁有兩個數據分區,且每個分區都對應一個獨立的文件目錄,用於保存各自部分的數據。

合理設計分區鍵非常重要,通常會按照數據表的查詢場景進行針對性設計。例如在剛才示例中的數據表按年月分區,如果后續的查詢按照分區鍵過濾,例如:

SELECT * FROM partition_v1 WHERE EventDate ='2019-05-01'

那么在后續的查詢過程中,可以利用分區索引跳過 6 月份的分區目錄,只加載 5 月份的數據,從而帶來查詢的性能提升。

當然,使用不合理的分區鍵也會適得其反,分區鍵不應該使用粒度過細的數據字段。例如按照小時分區,將會帶來分區數量的急劇增長,從而導致性能下降。關於數據分區更詳細的原理說明,也會在后續章節進行。

視圖

ClickHouse 擁有普通和物化兩種視圖,其中物化視圖擁有獨立的存儲,我們一會說;而普通視圖和關系型數據庫中的視圖類似,只是一層簡單的查詢代理。創建普通視圖的完整語法如下所示:

CREATE VIEW [IF NOT EXISTS] [db_name.]view_name AS SELECT...

普通視圖不會存儲任何數據,它只是一層單純的 SELECT 查詢映射,起着簡化查詢、明晰語義的作用,對查詢性能不會有任何增強。假設有一張普通視圖 view_tb_v1,它是基於數據表 tb_v1 的 id 和 name 兩個字段創建的,那么下面的兩條 SELECT 查詢是完全等價的:

SELECT * FROM view_tb_v1;
SELECT id, name FROM tb_v1;

而物化視圖需要指定表引擎,數據保存形式由它的表引擎決定,創建物化視圖的完整語法如下所示:

CREATE MATERIALIZED VIEW [IF NOT EXISTS] [db.]view_name [TO [db.]name] ENGINE = engine [POPULATE] AS SELECT ...

我們來對這些語法規則舉例說明,我們先來創建一張物化視圖:

-- 物化視圖本質上可以看成是一張特殊的數據表,在創建的時候也需要指定引擎
CREATE MATERIALIZED VIEW girls_view_1 ENGINE=TinyLog()
AS SELECT id, name, age FROM girls;

可以看到相較於普通視圖,我們在創建物化的視圖的時候,在 create 后面多寫了一個 MATERIALIZED 來表示創建的是物化視圖、以及指定了一個表引擎。但需要注意的是,物化視圖是可以存儲數據的。比如此時的物化視圖 girls_view_1 是根據表 girls 的 id、name、age 三個字段創建的,如果之后再往 girls 里面寫數據,那么新寫入的數據對應的 id、name、age 就會同步到 girl_view_1 中。也就是說,物化視圖創建好之后,如果源表被寫入新數據,那么物化視圖也會同步更新。

但需要注意的是,默認情況下,物化視圖在創建時不會拷貝源表中的數據,它只會同步在此之后被寫入源表的數據,所以當前 girls 里面的已存在的數據並沒有寫入到 girls_view_\1 中。如果希望在創建的物化視圖的時候,就順帶把表中的數據也同步過去該怎么做呢?

-- 只需要在 AS SELECT 的前面加上 POPULATE 即可
-- 此時表 girls 的數據,更准確的說是 SELECT 查詢得到的結果集才會進入物化視圖中
CREATE MATERIALIZED VIEW girls_view_1 ENGINE=TinyLog()
POPULATE AS SELECT id, name, age FROM girls;

所以 POPULATE 修飾符決定了物化視圖的初始化策略:如果使用了 POPULATE 修飾符,那么在創建視圖的過程中,會連帶將源表中已存在的數據(更准確的說是 SELECT 查詢得到的結果集)一並導入。反之,如果不使用 POPULATE 修飾符,那么物化視圖在創建之后是沒有數據的,它只會同步在此之后被寫入源表的數據。

另外物化視圖本質是一張特殊的數據表,如果存在的話,那么使用 SHOW TABLE 也能查看到。而如果刪除一個視圖,則直接使用 DROP TABLE 即可,注意:沒有 DROP VIEW,只要是視圖,刪除語句都是 DROP TABLE,所以這也側面說明了視圖名和表名不可以重復。

然后物化視圖還有一個用法,首先創建一個物化視圖的時候其實本質上還是會創建一張表,默認名稱是 " .inner.物化視圖名 ",只不過這張表是隱藏的,其作用就是負責保存物化視圖從源表中同步過來的數據。那么問題來了,負責存儲物化視圖數據的表可不可以我們自己指定呢?答案是可以的。

CREATE MATERIALIZED VIEW girls_view_1 TO other_girls
AS SELECT id, name, age FROM girls;

物化視圖在同步數據的時候就會將數據寫入到 other_girls 中,當然 other_girls 的表結構與 SELECT 選擇的字段的類型、數量要相匹配,並且此時我們既可以通過物化視圖查看數據,也可以通過 other_girls 來查看。這種用法,我們后面介紹表引擎的時候會說,目前先了解物化視圖的用法。

數據表的基本操作

目前只有 MergeTree、Merge 和 Distributed 這三類表引擎支持 ALTER 查詢,如果現在還不明白這些表引擎的作用也不必擔心,目前只需簡單了解這些信息即可,后面會對它們進行介紹。

追加新字段

假如需要對一張數據表追加新的字段,可以使用如下語法:

ALTER TABLE table_name ADD COLUMN [IF NOT EXISTS] 字段名 [類型] [默認值] [插在哪個字段后面]

修改字段類型

如果需要改變表字段的數據類型或者默認值,需要使用下面的語法:

ALTER TABLE table_name MODIFY COLUMN [IF NOT EXISTS] 字段名 [類型] [默認值]

修改某個字段的數據類型,實質上會調用相應的 toType 轉型方法。如果當前的類型與期望的類型不能兼容,則修改類型失敗。例如將 String 類型的 IP 字段轉成 IPv4 類型是可行的,但是轉成 UInt 則會出現錯誤。

修改備注

做好信息備注是保持良好編程習慣的美德之一,所以如果你還沒有為列字段添加備注信息,那么就趕緊行動吧。追加備注的語法如下所示:

ALTER TABLE table_name COMMENT COLUMN [IF EXISTS] 字段名 'some comment'

刪除已有字段

假如要刪除某個字段,可以使用下面的語句:

ALTER TABLE table_name DROP COLUMN [IF EXISTS] name

如果某個字段被刪除,那么對應的數據也就被刪除了。

移動數據表

在 Linux 系統中,mv 命令的本意是將一個文件從原始位置 A 移動到目標位置 B,但是如果位置 A 與位置 B 相同,則可以變相實現重命名的作用。ClickHouse 的 RENAME 查詢就與之有着異曲同工之妙,RENAME 語句的完整語法如下所示:

RENAME TABLE [db_name1.]table_name1 TO [db_name2.]table_name2, [db_name1.]table_name3 TO [db_name2.]table_name3......

RENAME 可以修改數據表的名稱,如果將原始數據庫與目標數據庫設為不同的名稱,那么就可以實現數據表在兩個數據庫之間移動的效果,並且還可以同時移動多張。

需要注意的是,數據表的移動只能在單個節點的范圍內。換言之,數據表移動的目標數據庫和原始數據庫必須在同一個服務節點內,而不能是集群中的遠程節點。

清空數據表

假設需要將表內的數據全部清空,而不是直接刪除這張表,則可以使用 TRUNCATE 語句,它的完整語法如下所示:

TRUNCATE TABLE [IF EXISTS] [db_name.]table_name

數據分區的基本操作

了解並善用數據分區益處頗多,熟練掌握它的使用方法,可以為后續的程序設計帶來極大的靈活性和便利性,目前只有 MergeTree 系列的表引擎支持數據分區。

查詢分區信息

ClickHouse 內置了許多 system 系統表,用於查詢自身的狀態信息,其中 parts 系統表專門用於查詢數據表的分區信息。例如執行下面的語句,就能夠得到數據表 partition_v1 的分區狀況:

如上所示,目前 partition_v1 共擁有 2 個分區,其中 partition_id 或者 name 等同於分區的主鍵,可以基於它們的取值確定一個具體的分區。

刪除指定分區

合理地設計分區鍵並利用分區的刪除功能,就能夠達到數據更新的目的,刪除一個指定分區的語法如下所示:

ALTER TABLE table_name DROP PARTITION partition_expr

假如現在需要更新 partition_v1 數據表整個 6 月份的數據,則可以先將 6 月份的分區刪除;

ALTER TABLE partition_v1 DROP PARTITION 201906

然后將整個 6 月份的新數據重新寫入,就可以達到更新的目的。

復制分區數據

ClickHouse 支持將 A 表的分區數據復制到 B 表,這項特性可以用於快速數據寫入、多表間數據同步和備份等場景,它的完整語法如下:

ALTER TABLE B REPLACE PARTITION partition_expr FROM A

不過需要注意的是,並不是任意數據表之間都能夠相互復制,它們還需要滿足兩個前提條件:

  • 兩張表需要擁有相同的分區鍵;
  • 它們的表結構完全相同;

假設有一個數據表 partition_v2,並且與之前 partition_v1 的分區鍵和表結構完全相同,那么如果想將 partition_v1 中 5 月份的數據導入到 partition_v2中,就可以這么做。

ALTER TABLE partition_v2 REPLACE PARTITION 201905 FROM partition_v1

重置分區數據

如果數據表某一列的數據有誤,需要將其重置為默認值,此時可以使用下面的語句實現:

ALTER TABLE table_name CLEAR COLUMN column_name IN PARTITION partition_expr

首先如果聲明了默認值表達式,那么以表達式為准;否則以相應數據類型的默認值為准,比如 String 類型的默認值就是空字符串。

裝載與卸載分區

表分區可以通過 DETACH 語句卸載,分區被卸載后,它的物理數據並沒有刪除,而是被轉移到了當前數據表目錄的 detached 子目錄下。而裝載分區則是反向操作,它能夠將 detached 子目錄下的某個分區重新裝載回去。卸載與裝載這一對伴生的操作,常用於分區數據的遷移和備份場景。卸載某個分區的語法如下所示:

ALTER TABLE table_name DETACH PARTITION partition_expr

假設有一個分區表 partition_v3,里面有很多月的數據,那么執行下面的語句就可以將該表中整個 8 月份的分區卸載。

ALTER TABLE partition_v3 DETACH PARTITION 201908

此時再次查詢這張表,會發現其中 2019 年 8 月份的數據已經沒有了。而進入 partition_v3 的磁盤目錄,則可以看到被卸載的分區目錄已經被移動到了 detached 目錄中。

記住,一旦分區被移動到了 detached 子日錄,就代表它已經脫離了 ClickHouse 的管理,ClickHouse 並不會主動清理這些文件。這此分區文件會一直存在,除非我們主動刪除或者使用 ATTACH 語句重新裝載它們。裝載某個分區的完整語法如下所示:

ALTER TABLE table_name ATTACH PARTITION partition_expr

再次執行下面的語句,就可以將剛才已被卸載的 201908 分區重新裝載回去:

ALTER TABLE partition_v3 ATTACH PARTITION 201908

備份與還原文件

關於分區數據的備份,可以通過 FREEZE 與 FETCH 實現,由於目前還缺少相關的背景知識,所以我們在后續章節介紹。

分布式 DDL 執行

ClickHouse 支持集群模式,一個集群擁有 1 到多個節點。CREATE、ALTER、DROP、RENMAE 及 TRUNCATE 這些 DDL 語句,都支持分布式執行。這意味着,如果在集群中任意一個節點上執行 DDL 語句,那么集群中的每個節點都會以相同的順序執行相同的語句。這項特性意義非凡,它就如同批處理命令一樣,省去了需要依次去單個節點執行 DDL 的煩惱。

將一條普通的 DDL 語句轉換成分布式執行十分簡單,只需加上 ON CLUSTER cluster_name 聲明即可。例如,執行下面的語句后將會對 ch_cluster 集群內的所有節點廣播這條 DDL 語句。

CREATE TABLE partition_v4 ON CLUSTER ch_cluster(
    ID String,
    URL String,
    EventDate Date
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(EventDate)
ORDER BY ID

當然,如果現在執行這條語句是不會成功的。因為到目前為止還沒有配置過 ClickHouse 的集群模式,目前還不存在一個名為 ch_cluster 的集群,這部內容會放到后續章節展開說明。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM