打造次世代分析型資料庫(三):列存表最佳實踐

語言: CN / TW / HK

作者介紹

作者介紹:jennyerchen(陳再妮),PostgreSQL ACE成員,TDSQL PG開源版負責人,有多年分散式資料庫核心研發經驗,曾供職於百度資料庫團隊,加入騰訊後參與了TDSQL PG版異地多活、讀寫分離、Oracle相容等多個核心模組的研發,當前主要負責CDW PG的存算分離相關特性的研發工作。

景簡

CDW PG是騰訊自主研發的新一代分散式資料庫,其具備業界領先的資料分析能力,在提供大型資料倉庫處理能力的同時還能完整支援事務, 採用無共享的叢集架構,適用於PB級海量 OLAP 場景。

OLAP場景列存表的應用比較廣泛,而且一般資料量都非常大,會佔用很多的磁碟空間。列存高效儲存表,因為資料是按列儲存的,如果進行壓縮的話可以具備很高的壓縮比,大大節省磁碟空間。

壓縮解壓過程

資料壓縮解壓過程如下圖所示:

  1. 寫入時進行壓縮。

  2. 讀取時進行解壓。

  3. 壓縮分為輕量級壓縮和透明壓縮2種,並且可疊加使用。資料寫入時先經過輕量級壓縮排行編碼,然後編碼結果可再進行透明壓縮。資料讀取時根據壓縮時採用的演算法先經過透明解壓,然後再經過輕量級解碼最後返回給使用者。

  4. 針對壓縮表的xlog、使用者資料的磁碟儲存形態都是壓縮的,而記憶體buffer中需要計算使用的資料是解壓過的。

注:整個過程全自主實現,對使用者完全透明,使用者0感知。

壓縮實現

對資料進行壓縮能夠有效地減少磁碟IO以及資料儲存成本,但對資料的壓縮和解壓操作也會消耗額外的CPU資源、影響資料的訪問與儲存效能。所以壓縮是一個用CPU換取磁碟IO的過程,需要根據業務需求,由使用者來指定列存表建立時可以建立壓縮表,也可以建立非壓縮表(行存暫且不支援指定壓縮)。

對比各種透明壓縮演算法的壓縮解壓效能和壓縮比,zstd是壓縮比最高的,lz4是壓縮解壓效率最好的,因此我們選擇zstd和lz4這兩種壓縮演算法分別用於不同的壓縮級別:根據使用者設定需要高壓縮級別的採用zstd,需要快速壓縮解壓而不追求壓縮比的採用lz4。

輕量級壓縮演算法主要是使用字元編碼的方式,常用的有RLE(當資料存在大量連續的相同值時,會把重複的資料儲存為一個數據值和計數)、Delta(只儲存資料間的差異diff,適用於資料改變很小的場景)、Dict(先會檢查資料的重複值,如果某一值出現的次數達到要求則將其加入字典。列中的值將會直接指向字典中與其重複的值)。根據其原理可知:數字型別的用Delta壓縮後再對diff值用RLE可以達到很好的壓縮比,文字型別的用Dict更適合。

因此如果指定了壓縮表,則資料寫入時進行壓縮,並且核心會根據資料型別自適應選擇較優的壓縮演算法:

  • 文字型別

    ow模式只用lz4;

    middle模式優先dict壓縮,成功直接返回,dict壓縮不成功進入lz4壓縮;

    high模式優先dict壓縮,成功直接返回,dict壓縮不成功進入zstd壓縮。

  • 數字型別

    low模式在delta的壓縮基礎上再加上RLE壓縮;

    middle模式在delta的壓縮基礎上再加上RLE壓縮、lz4壓縮;

    high模式在delta的壓縮基礎上再加上RLE壓縮、zstd壓縮。

  • numeric型別

    壓縮級別為low時:numeric能轉成int32或者int64的,用 delta + RLE 壓縮,不能轉化的用lz4;

    壓縮級別為middle時:numeric能轉成int32或者int64的,用 delta + RLE 壓縮,不能轉化的用lz4;在前面的壓縮基礎上再加上lz4;

    壓縮級別為high時:numeric能轉成int32或者int64的,用delta + RLE 壓縮,不能轉化的用lz4;在前面的壓縮基礎上再加上zstd;

    壓縮過程中最終將使用的壓縮方法儲存在壓縮頁面頭部中,供後續解壓使用。資料讀取時會進行解壓,解壓時優先讀取頭部資訊,根據頭部資訊中記錄的壓縮演算法,然後呼叫對應演算法的解壓函式,解壓時的順序與壓縮時的順序相逆,先使用透明壓縮演算法進行解壓,再使用輕量級壓縮演算法進行解壓。

使用實踐

表級別壓縮

表級別壓縮的級別分為 high/middle/low/no,啟用壓縮可以降低磁碟儲存大小,級別越高,磁碟佔用空間越小,壓縮也越耗時。

表級別壓縮需要在建立表時指定壓縮級別:WITH (orientation=column, compression = $壓縮級別); 不指定壓縮則預設是low壓縮,可以通過compression = no 顯示指定不壓縮。

1)建立一張不壓縮表:

CREATE TABLE test1 (


id bigint NOT NULL,


name varchar(25) NOT NULL,


quantity numeric(15,2) NOT NULL,


commitdate date NOT NULL,


)WITH (orientation = column, compression = no);

2)建立一張low級別壓縮表:

CREATE TABLE test2 (


id bigint NOT NULL,


name varchar(25) NOT NULL,


quantity numeric(15,2) NOT NULL,


commitdate date NOT NULL,


)WITH (orientation = column);

或者

CREATE TABLE test3 (


id bigint NOT NULL,


name varchar(25) NOT NULL,


quantity numeric(15,2) NOT NULL,


commitdate date NOT NULL,


)WITH (orientation = column, compression = low);

3)建立一張middle級別壓縮表:

CREATE TABLE test4 (


id bigint NOT NULL,


name varchar(25) NOT NULL,


quantity numeric(15,2) NOT NULL,


commitdate date NOT NULL,


)WITH (orientation = column, compression = middle);

4)建立一張high級別壓縮表:

CREATE TABLE test5(


id bigint NOT NULL,


name varchar(25) NOT NULL,


quantity numeric(15,2) NOT NULL,


commitdate date NOT NULL,


)WITH (orientation = column, compression = high);

如果指定了壓縮表,則資料寫入時進行壓縮儲存,資料讀取時會自動進行解壓,整個過程完全透明,使用者無感知。

注:表的壓縮級別是建立表時就指定好的,不支援DDL變更。

列級別壓縮設計

列存表建表時除了可以指定整個表級別的壓縮級別外,還支援對某個列單獨指定壓縮級別,而不依賴整個表的壓縮情況,列的壓縮級別也分為 high/middle/low/no。

CREATE TABLE test6 (


id bigint NOT NULL,


name varchar(25) encoding(compression=high) NOT NULL,


quantity numeric(15,2) encoding(compression=middle) NOT NULL,


commitdate date encoding(compression=no) NOT NULL,


)WITH (orientation = column, compression = low);

列級別壓縮中不同欄位可指定不同的壓縮級別 high/middle/low/no,不用關心整個表的壓縮級別,沒有單獨指定壓縮欄位的列採用表的壓縮級別。

注:每個列的壓縮級別是建立表時就確定好的,不支援DDL變更。

分割槽表壓縮

因為分割槽表父表的列屬性全部自動被子表繼承,因此所有分割槽表子表的列壓縮級別也繼承自父表的列屬性的壓縮級別。而父表的列屬性的壓縮級別依賴於建立表時使用者的指定。

  • 建立range分割槽壓縮表,指定表級別壓縮+列級別壓縮

建立父表:

create table order_range(


id int not null,


userid integer,


product text encoding(compression=high),


createdate date not null


) partition by range ( createdate ) with(orientation=column, compression = middle);

建立子表:

create table order_range_201701 partition of order_range(id,userid,product, createdate) for values from ('2017-01-01') to ('2017-02-01') with(orientation=column);


create table order_range_201702 partition of order_range(id,userid,product, createdate) for values from ('2017-02-01') to ('2017-03-01') with(orientation=column);


create table order_range_default partition of order_range default with(orientation=column);

子表 order_range_201701、order_range_201702、order_range_default的情況都和父表的一樣:所有列只有product列是high壓縮級別,其他所有列都是middle壓縮級別。

  • 建立list分割槽表

建立父表:

create table order_list(


id int not null,


userid integer encoding(compression=no),


product text encoding(compression=high),


area text encoding(compression=middle),


createdate date encoding(compression=low)


) partition by list( area ) with(orientation=column);

建立子表:

create table order_list_gd partition of order_list(id,userid,product,area,createdate) for values in ('guangdong') with(orientation=column);


create table order_list_bj partition of order_list(id,userid,product,area,createdate) for values in ('beijing') with(orientation=column);


create table order_list_default partition of order_list default with(orientation=column);

子表 order_list_gd、order_list_bj、order_list_default的情況都和父表的一樣:其中id列是low壓縮級別,userid列不壓縮,product是high壓縮級別,area是middle壓縮級別,createdate是low壓縮級別。

因為父表沒有指定compression引數,所以預設為low,id列沒有額外指定,所以預設繼承了表的low壓縮級別。

  • 建立hash分割槽表

建立父表:

CREATE TABLE orders_hash (


order_id bigint encoding(compression=no) not null,


cust_id bigint not null,


status text encoding(compression=middle)


) PARTITION BY HASH (order_id) with(orientation=column);

建立子表:

CREATE TABLE orders_p1 PARTITION OF orders_hash FOR VALUES WITH (MODULUS 4, REMAINDER 0) with(orientation=column);


CREATE TABLE orders_p2 PARTITION OF orders_hash FOR VALUES WITH (MODULUS 4, REMAINDER 1) with(orientation=column);


CREATE TABLE orders_p3 PARTITION OF orders_hash FOR VALUES WITH (MODULUS 4, REMAINDER 2) with(orientation=column);


CREATE TABLE orders_p4 PARTITION OF orders_hash FOR VALUES WITH (MODULUS 4, REMAINDER 3) with(orientation=column);

子表 orders_p1、orders_p2、orders_p3、orders_p4的情況都和父表的一樣:其中order_id列是不壓縮,cust_id是low壓縮級別,status是middle壓縮級別。

因為父表沒有指定compression引數,所以預設為low,cust_id列沒有額外指定,所以預設繼承了表的low壓縮級別。

  • 建立行列混合分割槽表,父表為列存表

建立父表:

create table t_hash_partition(


f1 int,


f2 int encoding(compression=low)


) partition by hash(f2) with(orientation=column, compression = middle);

建立子表:

create table t_hash_partition_1 partition of t_hash_partition FOR VALUES WITH(MODULUS 4, REMAINDER 0) with(orientation=column);


create table t_hash_partition_2 partition of t_hash_partition FOR VALUES WITH(MODULUS 4, REMAINDER 1);


create table t_hash_partition_3 partition of t_hash_partition FOR VALUES WITH(MODULUS 4, REMAINDER 2) with(orientation=column);


create table t_hash_partition_4 partition of t_hash_partition FOR VALUES WITH(MODULUS 4, REMAINDER 3)with(orientation='row');

子表t_hash_partition_1、t_hash_partition_3為列存表,f1列是繼承自父表的middle壓縮級別,f2列是low壓縮級別;

子表t_hash_partition_2、t_hash_partition_4為行存表,f1 f2列不進行壓縮(行存表不繼承壓縮屬性);

因為建表時不指定orientation引數時,預設為行存格式,所以t_hash_partition_2 為行存表。

  • 建立行列混合分割槽表,父表為行存表

建立父表:

CREATE TABLE orders_mix (


order_id bigint not null,


cust_id bigint not null,


status text


) PARTITION BY HASH (order_id);

建立子表:

CREATE TABLE orders_mix_p1 PARTITION OF orders_mix FOR VALUES WITH (MODULUS 4, REMAINDER 0);


CREATE TABLE orders_mix_p2 PARTITION OF orders_mix FOR VALUES WITH (MODULUS 4, REMAINDER 1) with(orientation=column);


CREATE TABLE orders_mix_p3 PARTITION OF orders_mix FOR VALUES WITH (MODULUS 4, REMAINDER 2);


CREATE TABLE orders_mix_p4 PARTITION OF orders_mix FOR VALUES WITH (MODULUS 4, REMAINDER 3) with(orientation=column);

子表orders_mix_p1、orders_mix_p3為行存表(因為建表時不指定orientation引數時,預設為行存格式);

子表orders_mix_p2、orders_mix_p4為列存表,因為父表為行表,沒有壓縮屬性所以orders_mix_p2、orders_mix_p4表所有列都不會進行壓縮。

壓縮結果測試

針對TPCH 1T資料量lineitem表在設定為不同壓縮級別的測試情況如下:

推薦閱讀

關注騰訊雲大資料公眾號

邀您探索資料的無限可能

點選“閱讀原文”,瞭解相關產品最新動態

↓↓↓