HStore表全瞭解:實時入庫與高效查詢利器

2023-06-14 18:19:24
摘要:本文章將從使用者角度介紹HStore概念以及使用。

本文分享自華為雲社群《GaussDB(DWS)HStore表講解》,作者:大威天龍:- 。

HStore表簡介

面對實時入庫和實時查詢要求越來越高的趨勢,已有的列儲存無法支援並行更新入庫,行存查詢效能無法做到實時返回且空間壓縮表現不佳。GaussDB(DWS)基於列儲存格式設計和實現了全新的HStore表,同時提供高效的並行插入、更新入庫,以及高效能實時查詢。本文章將從使用者角度介紹HStore概念以及使用。

HStore表的背景

為什麼要有HStore表呢?在具體講解HStore表之前,我們先來回顧一下GaussDB(DWS)中幾種已有的表型別:

行存表(row-store)

最基礎的表型別,顧名思義,資料按行儲存,在實際的物理塊中,資料的將按下列圖示的方式儲存:

優勢很明顯,點查場景下,直接就能索引到行存某行元組的位置,點查效能好。資料庫中的系統表就是行存表,對於使用者的一些對點查效能要求高或者頻繁更新的小表,都推薦用行存表。

列存表(column-store)

AP場景下,常常需要對某列進行批次查詢來做分析業務,這時候採用行存的話就會把所有列都讀出來產生冗餘IO, 同時AP場景下的表資料量往往很大,行存表壓縮暫未商用,使用行存表也會導致佔用空間過大。

GaussDB(DWS)中的列存表就是針對這種場景實現的,列存表資料的實際儲存示意圖如下:

列存表將每列的資料批次儲存成一個CU(Compress Unit), 能帶來了很好的空間壓縮與批次查詢效能提升,對於一些涉及多表關聯的分析類複雜查詢、資料不經常更新的表,推薦使用列存表。

列存帶Delta表

對於列存表,如果業務是頻繁的小批次插入,那麼將產生大量的小CU(單個CU裡只有幾百條甚至幾條資料), 每個列的CU都是有壓縮代價的,小CU過多將嚴重影響列存表的查詢效能。

列存的Delta表就是針對這種場景實現的,讓小批次插入的資料先儲存到行存delta表,滿6w後由後臺autovacuum非同步merge到主表CU。

需要注意的是列存帶Delta表只解決小批次入庫產生的小CU問題,不解決同一個CU上的並行更新問題

HStore表

前面提到,雖然列存老Delta表解決了小批次入庫產生的小CU問題,但是沒有解決同一個CU上的並行更新產生的鎖衝突問題。

而實時入庫的場景下,需要將insert+upsert+update操作實時並行入庫,資料來源於上游的其他資料庫或者應用,同時要求入庫後的資料要能及時查詢,且對於查詢的效率要求很高。

目前的列存表由於鎖衝突的原因無法支援並行upsert/update入庫,導致這些有需要的局點只能使用行存表,但是行存表因為格式的天然劣勢,在AP查詢場景下一方面效能較慢,另一方面由於壓縮差導致佔用了大量的磁碟空間,對使用者產生額外成本。

GaussDB(DWS)中的HStore表, 在使用列儲存格式儘量降低磁碟佔用的同時,支援高並行的更新操作入庫以及高效能的查詢效率。面向對於實時入庫和實時查詢有較強訴求的場景,同時擁有處理傳統TP場景的事務能力。

HStore表的示意圖如下:

GaussDB(DWS) 中幾種表型別的對比

HStore的Delta表

HStore表的實現主要依靠一張新設計的delta表以及記憶體並行控制機制,這裡簡單講一下delta表的實現以及簡單的觀察delta表。

HStore的Delta表主要用於存放入庫產生的Insert/Delete/Update操作,小批次Insert的資料會先進入Delta形成一條型別是I(Insert)的記錄;刪除會往Delta表插入一條型別是D(Delete)的記錄;更新操作(Upsert與Update)會拆分成Delete + Insert,會插入一條型別X(表示由更新產生的刪除)的記錄以及一條型別I的記錄;
(型別是U(Update)的記錄由輕量化Update產生,不過當前輕量化更新預設關閉,所以不用管。)

可以看到,入庫時的Upsert/Update/Delete都會轉換成相應型別的記錄插入的HStore的Delta表中,再結合記憶體並行控制機制,就能保證同一個CU上更新於刪除操作不會阻塞。同時,由於小批次的插入只會在Delta表上形成一條記錄,相比與列存老Delta的直接儲存資料,能減少IO佔用,提高MERGE效率。

HStore的Delta表 與 列存老Delta表的對比

HStore的檢視與函數

當前HStore表提供了檢視,可以用來觀察Delta表的給型別元組數量以及Delta的膨脹情況。

select * from pgxc_get_hstore_delta_info('tableName');

同時也提供了函數可以對Delta表做輕量清理以及全量清理。

-- 輕量Merge滿6萬的I記錄以及CU上的刪除資訊,持有四級鎖不阻塞業務增刪改查,但空間不會還給作業系統。
select hstore_light_merge('tableName'); 
-- 全量Merge所有記錄,然後truncate清空Delta表返還空間給系統,不過持有八級鎖會阻塞業務。
select hstore_full_merge('tableName');

這裡做一個簡單的觀察實驗:

1.往HStore表上批次插入一百條資料,能看到生成了一條型別是I的記錄(n_i_tup 為1)

gaussdb=# create table data(a int primary key, b int);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "data_pkey" for table "data"
CREATE TABLE
gaussdb=# insert into data values(generate_series(1,100),1);
INSERT 0 100
gaussdb=# create table hs(a int primary key, b int)with(orientation=column, enable_hstore=on);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "hs_pkey" for table "hs"
CREATE TABLE
gaussdb=# insert into hs select * from data;
INSERT 0 100
gaussdb=# select * from pgxc_get_hstore_delta_info('hs'); --觀察hstore表的delta表上的各型別資料
 node_name | part_name | live_tup | n_i_type | n_d_type | n_x_type | n_u_type | n_m_type | data_size
-----------+---------------------+----------+----------+----------+----------+----------+----------+-----------
 dn_1      | non partition table | 1 | 1 | 0 | 0 | 0 | 0 | 8192
(1 row)

2.執行hstore_full_merge後能觀察到Delta表上沒有元組(live_tup為0),並且Delta表的空間大小data_size是0.

gaussdb=# select hstore_full_merge('hs');
 hstore_full_merge
-------------------
 1
(1 row)
gaussdb=# select * from pgxc_get_hstore_delta_info('hs'); --觀察hstore表的delta表上的各型別資料
 node_name | part_name | live_tup | n_i_type | n_d_type | n_x_type | n_u_type | n_m_type | data_size
-----------+---------------------+----------+----------+----------+----------+----------+----------+-----------
 dn_1      | non partition table | 0 | 0 | 0 | 0 | 0 | 0 | 0
(1 row)

3.執行刪除,能觀察到Delta表上有一條型別是D的記錄(n_d_tup為1)。

gaussdb=# delete hs where a = 1;
DELETE 1
gaussdb=# select * from pgxc_get_hstore_delta_info('hs'); --觀察hstore表的delta表上的各型別資料
 node_name | part_name | live_tup | n_i_type | n_d_type | n_x_type | n_u_type | n_m_type | data_size
-----------+---------------------+----------+----------+----------+----------+----------+----------+-----------
 dn_1      | non partition table | 1 | 0 | 1 | 0 | 0 | 0 | 8192
(1 row)

其它的操作這裡不再一一嘗試,感興趣的讀者可以自己下來試一下。

HStore表的簡單使用實驗

準備工作

當需要使用HStore表時,需要同步修改以下幾個清理相關的引數預設值,否則會導致HStore表效能嚴重劣化。推薦的引數修改設定是:autovacuum_max_workers_hstore=3,autovacuum_max_workers=6,autovacuum=true。

並行更新實驗

在列存表上插入一批資料後,開啟兩個對談,

1.對談1刪除某一條資料,然後不結束事務:

gaussdb=#  create table col(a int , b int)with(orientation=column);
CREATE TABLE
gaussdb=# insert into col select * from data;
INSERT 0 100
gaussdb=# begin;
BEGIN
gaussdb=# delete col where a = 1;
DELETE 1

2.對談2刪除另一條資料,能看到對談2等待對談1,

gaussdb=# begin;
BEGIN
gaussdb=# delete col where a = 2;

對談1提交後對談2才能繼續執行,這就復現了列存的CU鎖問題:

3. 使用HStore表重複上面實驗,能觀察到對談2直接執行成功,不會鎖等待。

gaussdb=# begin;
BEGIN
gaussdb=# delete hs where a = 2;
DELETE 1

壓縮效率實驗

1.構建一張有三百萬資料的資料表data

gaussdb=# create table data( a int, b bigint, c varchar(10), d varchar(10));
CREATE TABLE
gaussdb=# insert into data values(generate_series(1,100),1,'asdfasdf','gergqer');
INSERT 0 100
gaussdb=# insert into data select * from data;
INSERT 0 100
gaussdb=# insert into data select * from data;
INSERT 0 200
---迴圈插入,直到資料量達到三百萬
gaussdb=# insert into data select * from data;
INSERT 0 1638400
gaussdb=# select count(*) from data;
  count
---------
 3276800
(1 row)

2.批次匯入到行存表,觀察大小為223MB

gaussdb=# create table row (like data including all);
CREATE TABLE
gaussdb=# insert into row select * from data;
INSERT 0 3276800
gaussdb=#  select pg_size_pretty(pg_relation_size('row'));
 pg_size_pretty
----------------
 223 MB
(1 row)

3.批次匯入到列存表,觀察大小為3.5MB

gaussdb=# create table hs(a int, b bigint, c varchar(10),d varchar(10))with(orientation= column, enable_hstore=on);
CREATE TABLE
gaussdb=# insert into hs select * from data;
INSERT 0 3276800
gaussdb=#  select pg_size_pretty(pg_relation_size('hs'));
 pg_size_pretty
----------------
 3568 KB
(1 row)

4.總結

這個表結構比較簡單,資料也都是重複資料,所以HStore表的壓縮效果很好,一般情況下HStore表相比行存能有3-5倍的壓縮。

批次查詢效能實驗

還是使用上面建的表,這裡簡單驗證一下批次查詢

1.查詢行存表的第四列,耗時在4s左右

gaussdb=# explain analyze select d from data;
explain analye                                                               QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------
  id |          operation           |        A-time | A-rows | E-rows | Peak Memory  | E-memory | A-width | E-width | E-costs
 ----+------------------------------+----------------------+---------+---------+--------------+----------+---------+---------+----------
 1 | ->  Streaming (type: GATHER) | 4337.881 | 3276800 | 3276800 | 32KB         | | | 8 | 61891.00
 2 | ->  Seq Scan on data | [1571.995, 1571.995] | 3276800 | 3276800 | [32KB, 32KB] | 1MB      | | 8 | 61266.00

2.查詢HStore表的第四列,耗時300毫秒左右

gaussdb=# explain analyze select d from hs;
                                                                    QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------
  id |               operation                |       A-time | A-rows | E-rows |  Peak Memory   | E-memory | A-width | E-width | E-costs
 ----+----------------------------------------+--------------------+---------+---------+----------------+----------+---------+---------+----------
 1 | -> Row Adapter                        | 335.280 | 3276800 | 3276800 | 24KB           | | | 8 | 15561.80
 2 | ->  Vector Streaming (type: GATHER) | 111.492 | 3276800 | 3276800 | 96KB           | | | 8 | 15561.80
 3 | -> CStore Scan on hs | [111.116, 111.116] | 3276800 | 3276800 | [254KB, 254KB] | 1MB      | | 8 | 14936.80

3.總結

這裡只驗證了批次查詢場景,該場景下列存以及HStore表相比行存都有很好的查詢效能。但在索引點查詢場景下,列存是比不上行存的,這裡不再做詳細對比。

HStore表注意事項

1.引數設定

HStore依賴後臺常駐執行緒對HStore表進行MERGE清理操作,才能保證查詢效能與壓縮效率,所以使用HStore表務必設定相關GUC,推薦的設定如下:

autovacuum_max_workers_hstore=3
autovacuum_max_workers=6
autovacuum=true

2.並行同一行:

當前HStore並行更新同一行仍然是不支援的,其中同一行上並行update/delete操作會先等鎖然後報錯,同一行上的並行upsert操作會先等鎖然後繼續執行。由於等待開銷也是會影響業務的入庫效能,甚至可能產生死鎖,所以需要在入庫時保證不會並行更新到同一行或者同一個key。

3.索引相關

索引會佔用額外的空間,同時帶來的點查效能提升有限,所以HStore表只建議在需要做Upsert或者有點查(這裡指唯一性與接近唯一的點查)的訴求下建立一個主鍵或者btree索引。

4.MERGE相關

由於HStore表依賴後臺autovacuum來將操作MERGE到主表,所以入庫速度不能超過MERGE速度,否則會導致delta表的膨脹,可以通過控制入庫的並行來控制入庫速度。同時由於Delta表本身的空間複用受oldestXmin的影響,如果有老事務存在可能會導致Delta空間複用不及時而產生膨脹。

5.UPSERT效能

HStore表雖然相比普通列存,並行upsert入庫效能得到了很大提升,但相比行存還是有差距,大概只有行存的1/3。所以在不追求壓縮率以及批次查詢效能、只追求單點查詢效能的場景下,還是推薦行存表入庫。

 

點選關注,第一時間瞭解華為雲新鮮技術~