摘要:本文章將從使用者角度介紹HStore概念以及使用。
本文分享自華為雲社群《GaussDB(DWS)HStore表講解》,作者:大威天龍:- 。
面對實時入庫和實時查詢要求越來越高的趨勢,已有的列儲存無法支援並行更新入庫,行存查詢效能無法做到實時返回且空間壓縮表現不佳。GaussDB(DWS)基於列儲存格式設計和實現了全新的HStore表,同時提供高效的並行插入、更新入庫,以及高效能實時查詢。本文章將從使用者角度介紹HStore概念以及使用。
為什麼要有HStore表呢?在具體講解HStore表之前,我們先來回顧一下GaussDB(DWS)中幾種已有的表型別:
最基礎的表型別,顧名思義,資料按行儲存,在實際的物理塊中,資料的將按下列圖示的方式儲存:
優勢很明顯,點查場景下,直接就能索引到行存某行元組的位置,點查效能好。資料庫中的系統表就是行存表,對於使用者的一些對點查效能要求高或者頻繁更新的小表,都推薦用行存表。
AP場景下,常常需要對某列進行批次查詢來做分析業務,這時候採用行存的話就會把所有列都讀出來產生冗餘IO, 同時AP場景下的表資料量往往很大,行存表壓縮暫未商用,使用行存表也會導致佔用空間過大。
GaussDB(DWS)中的列存表就是針對這種場景實現的,列存表資料的實際儲存示意圖如下:
列存表將每列的資料批次儲存成一個CU(Compress Unit), 能帶來了很好的空間壓縮與批次查詢效能提升,對於一些涉及多表關聯的分析類複雜查詢、資料不經常更新的表,推薦使用列存表。
對於列存表,如果業務是頻繁的小批次插入,那麼將產生大量的小CU(單個CU裡只有幾百條甚至幾條資料), 每個列的CU都是有壓縮代價的,小CU過多將嚴重影響列存表的查詢效能。
列存的Delta表就是針對這種場景實現的,讓小批次插入的資料先儲存到行存delta表,滿6w後由後臺autovacuum非同步merge到主表CU。
需要注意的是列存帶Delta表只解決小批次入庫產生的小CU問題,不解決同一個CU上的並行更新問題
前面提到,雖然列存老Delta表解決了小批次入庫產生的小CU問題,但是沒有解決同一個CU上的並行更新產生的鎖衝突問題。
而實時入庫的場景下,需要將insert+upsert+update操作實時並行入庫,資料來源於上游的其他資料庫或者應用,同時要求入庫後的資料要能及時查詢,且對於查詢的效率要求很高。
目前的列存表由於鎖衝突的原因無法支援並行upsert/update入庫,導致這些有需要的局點只能使用行存表,但是行存表因為格式的天然劣勢,在AP查詢場景下一方面效能較慢,另一方面由於壓縮差導致佔用了大量的磁碟空間,對使用者產生額外成本。
GaussDB(DWS)中的HStore表, 在使用列儲存格式儘量降低磁碟佔用的同時,支援高並行的更新操作入庫以及高效能的查詢效率。面向對於實時入庫和實時查詢有較強訴求的場景,同時擁有處理傳統TP場景的事務能力。
HStore表的示意圖如下:
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的膨脹情況。
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表效能嚴重劣化。推薦的引數修改設定是: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表相比行存都有很好的查詢效能。但在索引點查詢場景下,列存是比不上行存的,這裡不再做詳細對比。
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。所以在不追求壓縮率以及批次查詢效能、只追求單點查詢效能的場景下,還是推薦行存表入庫。