閱讀993 返回首頁    go 阿裏雲 go 技術社區[雲棲]


高階魔方與數據編排 - 數據庫存儲優化之路

標簽

PostgreSQL , cluster , 預排 , 一維數據 , 多維數據 , 視覺編排 , 數據編排 , IO優化


背景

中華文化源遠流長,比如這句古語“遠水不救近火,遠親不如近鄰”,在數據庫的優化中亦有體現。接下來我們來揭示這個道理。

大多數數據庫的存儲為塊存儲,一個塊裏麵可能涉及到多條記錄,當用戶輸入查詢條件進行數據檢索時,即使返回的結果集較小,也可能需要掃描多個數據塊,因為你不知道你要的記錄落在哪些數據塊裏麵。

例子

隨機寫入一批數據

create table tbl(id int, info text default repeat(random()::text,10), crt_time timestamp default now());  
insert into tbl (id) select random()*10000 from generate_series(1,1000000);  

查詢ID<100的記錄,這些記錄落在哪些數據塊裏麵?

postgres=# select ctid from tbl where id<100;  
    ctid      
------------  
 (4,10)  
 (6,32)  
 (7,33)  
 (17,14)  
 (22,15)  
 (22,16)  
 (25,2)  
 (29,21)  
 (41,13)  
 (42,27)  
 (43,27)  
 (54,11)  
 (54,33)  
 (56,1)  
 (60,13)  
。。。。。。  

可以看到,數據散落在不同的數據塊裏麵。也就是說訪問了很多數據塊。

很多用戶遇到了類似的問題,我做了一些總結和優化之道,請參考下麵的文章。

《索引順序掃描引發的堆掃描IO放大背後的統計學原理與解決辦法》

《懶人推動社會進步 - 多列聚合, gin與數據分布(選擇性)》

《索引掃描優化之 - GIN數據重組優化(按元素聚合) 想象在玩多階魔方》

以上文章提到了優化的方法:數據重新編排存儲。讓數據盡量的根據搜索條件進行聚集,減少掃描成本。

數據存儲優化 - 編排

數據存儲編排的目的很簡單,讓數據盡量的根據搜索條件進行聚集,減少掃描成本。

但是數據種類不一樣,編排優化也不太一樣。前麵提到的例子針對的都是一維的數據,一維數據的操作很簡單,=, >, <, >=, <=, in等。

如果數據類型不是一維類型,而是多值類型或者異構類型呢?例如數組、空間類型、全文檢索類型等。數據的編排還有這麼簡單嗎?

一維數據編排

一維類型的編排,按搜索列排序是最簡單有效的編排方法。業界也叫預排序,PostgreSQL術語叫cluster,通過cluster命令即可完成編排。

https://www.postgresql.org/docs/9.6/static/sql-cluster.html

多維數據編排

一位編排比較簡單,但是多維編排會更加複雜。我通過一個遊戲來說明編排的目的。

我們來玩一個遊戲,假設全球有10000個國家,每個國家挑選若幹人(每個國家挑選的人數可以不一樣,例如A國家挑選了1000人,B國家挑選了10人,假設總共挑選了100萬人)進行混合分組,每個分組包含了若幹不同國家的人(每個分組每個國家僅限1人)。下麵進行排隊,橫坐標為分組ID,縱坐標為國家ID。接下來要進行搬運遊戲,從左導遊,每個國家的人,需要將它手裏的球搬運給右邊的相同國家的人,如果他們相鄰,則不計成本。如果他們之間隔了其他分組,則計算成本(相隔的分組個數)。最終計算每個國家的搬運成本,某些國家的搬運成本,或者所有國家相加的總搬運成本。

如何降低總成本,如何降低某些國家的成本,如何降低某個國家的成本?

例如第一種排列方式,1號國家的搬運成本為2, 6號,7號國家的搬運成本為1,其他國家的搬運成本為0。

pic

第二種排列方式如下,所有國家的搬運成本都為0。

pic

這就是數組類型編排要取得的目的,由於數組類型通常是按數組的元素進行檢索的,因此我們需要根據元素來進行編排。數組類型的編排方式同樣適用於全文檢索類型,TOKEN類型。

其他類型的編排則根據數據類型來定,例如空間類型,可以根據GEOHASH的值進行排序編排。

下麵我們講一下數組類型編排的實現過程。

生成測試數據

生成隨機數組的方法

postgres=# create or replace function gen_array(range int, cnt int) returns int[] as $$  
select array(select (random()*range)::int from generate_series(1,cnt) group by 1);  
$$ language sql strict volatile;  
CREATE FUNCTION  
  
postgres=# select gen_array(10,5);  
 gen_array   
-----------  
 {9,1,5,8}  
(1 row)  
  
postgres=# select gen_array(10, 20);  
       gen_array          
------------------------  
 {9,3,1,4,5,2,7,6,8,10}  
(1 row)  
  
postgres=# select gen_array(100,10) from generate_series(1,100);  
            gen_array               
----------------------------------  
 {9,16,51,72,31,27,20,63,41,65}  
 {88,1,59,39,87,28,24,32,21}  
 {69,78,34,11,84,79,97,80,85,56}  
 {3,90,42,81,83,41,76,99,65,25}  
 {14,4,42,50,37,13,72,75,29,74}  
 {23,92,53,74,77,71,93,82}  
 {1,16,81,51,79,76,62,94,77}  
 {48,64,18,35,43,36,54,41,29,98}  
 {100,84,28,99,80,8,85,77,10,33}  
 {96,31,39,7,87,53,36,54,30,77}  
 {70,49,90,91,13,40,31,54,97,94}  
 {58,55,35,75,29,6,65,56,98}  
......  

建立測試表,寫入隨機數組

postgres=# create table tbl2(id int, arr int[]);  
CREATE TABLE  
  
postgres=# insert into tbl2 select id, gen_array(10,10) from generate_series(1,10) t(id);  
INSERT 0 10  
  
postgres=# select * from tbl2 limit 10;  
 id |         arr           
----+---------------------  
  1 | {1,5,2,6,8}  
  2 | {1,4,5,2,7,6,8,0}  
  3 | {9,1,5,2,7,6,8}  
  4 | {3,1,4,5,2,7,6}  
  5 | {9,1,5,2,7,8,10}  
  6 | {9,3,1,2,7,6,8}  
  7 | {9,3,1,5,2,7,8}  
  8 | {3,1,4,5,2,6,10}  
  9 | {9,3,1,4,5,2,7,6,8}  
 10 | {3,1,5,7,6,10,0}  
(10 rows)  

10條記錄,有多少種排列組合的可能呢?

PostgreSQL有排列組合函數可以支持,如下。

postgres=# \df *.*fac*  
                             List of functions  
   Schema   |    Name     | Result data type | Argument data types |  Type    
------------+-------------+------------------+---------------------+--------  
 pg_catalog | factorial   | numeric          | bigint              | normal  
 pg_catalog | numeric_fac | numeric          | bigint              | normal  
(2 rows)  
  
postgres=# select factorial(10);  
 factorial   
-----------  
   3628800  
(1 row)  
  
postgres=# select numeric_fac(10);  
 numeric_fac   
-------------  
     3628800  
(1 row)  
  
postgres=# select 10*9*8*7*6*5*4*3*2;  
 ?column?   
----------  
  3628800  
(1 row)  
  
postgres=# select 10!;  
 ?column?   
----------  
  3628800  
(1 row)  
  
postgres=# \do+ !  
                                      List of operators  
   Schema   | Name | Left arg type | Right arg type | Result type |  Function   | Description   
------------+------+---------------+----------------+-------------+-------------+-------------  
 pg_catalog | !    | bigint        |                | numeric     | numeric_fac | factorial  
(1 row)  

排列組合

排列組合,指行的排列順序。下麵兩篇文檔中也涉及排列組合的例子。

《PostgreSQL n階乘計算, 排列組合計算 - 如何計算可變參數中有沒有重複參數》

《為什麼啤酒和紙尿褲最搭 - 用HybridDB/PostgreSQL查詢商品營銷最佳組合》

pic

通過行號的順序,得到排列組合,驗證通過。

postgres=#  WITH RECURSIVE cte AS (  
     SELECT array[ctid] AS combo, ctid, 1 AS ct   
     FROM tbl2  
   UNION ALL   
     SELECT array_append(cte.combo, t.ctid), t.ctid, ct + 1   
     FROM cte, tbl2 t  
     WHERE ct <= 10  -- 10是 tbl2的count(*),即自關聯10次,得到所有排列組合  
       AND array_position(cte.combo, t.ctid) is null  -- 元素去重  
)   
SELECT count(combo) FROM cte where ct=10;  -- 10是 tbl2的count(*)  
  
  count    
---------  
 3628800  
(1 row)  

排列組合示例:

postgres=#  WITH RECURSIVE cte AS (    
     SELECT array[ctid] AS combo, ctid, 1 AS ct   
     FROM tbl2  
   UNION ALL   
     SELECT array_append(cte.combo, t.ctid), t.ctid, ct + 1   
     FROM cte, tbl2 t  
     WHERE ct <= (select count(*) from tbl2)   
       AND array_position(cte.combo, t.ctid) is null  
)   
SELECT combo FROM cte where ct=10 limit 10;  
                                       combo                                          
------------------------------------------------------------------------------------  
 {"(0,1)","(0,2)","(0,3)","(0,4)","(0,5)","(0,6)","(0,7)","(0,8)","(0,9)","(0,10)"}  
 {"(0,1)","(0,2)","(0,3)","(0,4)","(0,5)","(0,6)","(0,7)","(0,8)","(0,10)","(0,9)"}  
 {"(0,1)","(0,2)","(0,3)","(0,4)","(0,5)","(0,6)","(0,7)","(0,9)","(0,8)","(0,10)"}  
 {"(0,1)","(0,2)","(0,3)","(0,4)","(0,5)","(0,6)","(0,7)","(0,9)","(0,10)","(0,8)"}  
 {"(0,1)","(0,2)","(0,3)","(0,4)","(0,5)","(0,6)","(0,7)","(0,10)","(0,8)","(0,9)"}  
 {"(0,1)","(0,2)","(0,3)","(0,4)","(0,5)","(0,6)","(0,7)","(0,10)","(0,9)","(0,8)"}  
 {"(0,1)","(0,2)","(0,3)","(0,4)","(0,5)","(0,6)","(0,8)","(0,7)","(0,9)","(0,10)"}  
 {"(0,1)","(0,2)","(0,3)","(0,4)","(0,5)","(0,6)","(0,8)","(0,7)","(0,10)","(0,9)"}  
 {"(0,1)","(0,2)","(0,3)","(0,4)","(0,5)","(0,6)","(0,8)","(0,9)","(0,7)","(0,10)"}  
 {"(0,1)","(0,2)","(0,3)","(0,4)","(0,5)","(0,6)","(0,8)","(0,9)","(0,10)","(0,7)"}  
(10 rows)  

統計每一種排列組合,各個元素的檢索成本

計算每一種排列組合,對應數組元素的聚合度(越靠近0(越小),越聚合)

根據前麵的聚合度算法,使用如下函數實現。

create or replace function cal_affinity(OUT o_combo tid[], OUT affinity int8) returns setof record as $$  
declare  
  ccc int8;     -- tbl2總共多少條記錄  
  v_tid tid[];  -- 行號組合  
  vv_tid tid;   -- 行號  
  i1 int := 1;  -- 正處於第幾行  
  i2 int[];     -- 與tbl2.arr同類型的數組,存儲所有元素  
  i3 int[];     -- 上一次元素出現在第幾行,下標=i2 中元素的位置  
  i4 int[];     -- 每個元素的聚合度累計值  
  v_i4 int;     -- 每個元素單獨的聚合度  
  sum_i4 int8;  -- i4的sum  
  x int;        -- 第幾個循環  
  v_arr int[];  -- tbl2 數組  
  i_arr int;    -- tbl2 數組元素  
begin  
  select count(*) into ccc from tbl2;  
  select array(select distinct(unnest(arr)) from tbl2) into i2;  
  
-- 排列組合循環  
for v_tid in   
  WITH RECURSIVE cte AS (    
       SELECT array[ctid] AS combo, ctid, 1 AS ct   
       FROM tbl2  
     UNION ALL   
       SELECT array_append(cte.combo, t.ctid), t.ctid, ct + 1   
       FROM cte, tbl2 t  
       WHERE ct <= ccc  
         AND array_position(cte.combo, t.ctid) is null  
  )   
  SELECT combo FROM cte where ct=ccc limit 10   --  隻計算10種組合  
  -- 3628800種組合實在太多了,我挑選一些進行計算,驗證聚合度結果。  
loop  
    
  -- 按行號循環  
  foreach vv_tid in array v_tid   
  loop  
      
    -- 生成tbl2當前行的數組  
    select arr into v_arr from tbl2 where ctid=vv_tid;  
  
    -- 按tbl2數組元素循環,計算每個元素的聚合度累加  
    foreach i_arr in array v_arr  
    loop  
      -- 獲取元素下標 array_position(i2, i_arr)  
      -- i1=1,處於第一行  
  
      -- 計算聚合度,真實場景應該改成數據塊是否相鄰,而不是行號  
      if i1=1 then  
	-- 第一行  
	i4[array_position(i2, i_arr)] := 0;  
      else  
        -- 不是第一行  
	if i4[array_position(i2, i_arr)] is null then  
          -- 該元素第一次出現  
	  i4[array_position(i2, i_arr)] := 0;  
	else  
	  -- 該元素不是第一次出現  
	  i4[array_position(i2, i_arr)] := i4[array_position(i2, i_arr)] + greatest((i1 - i3[array_position(i2, i_arr)] - 1), 0);  -- 防止單行元素重複計算錯誤  
	end if;  
      end if;  
        
      -- 元素最後一次出現在第幾行  
      i3[array_position(i2, i_arr)] := i1;  
    end loop;  
  
    -- 行號+1  
    i1 := i1 + 1;  
  end loop;  
  
  -- 輸出該排列組合的所有元素的聚合度,總聚合度  
  select sum(unnest) into sum_i4 from (select unnest(i4)) t;  
  raise notice 'combo: %, sum(affinity): %', v_tid, sum_i4;  
  x := 1;  
  foreach v_i4 in array i4  
  loop  
    raise notice 'elements: %, affinity: %', i2[x], v_i4;  
    x := x+1;  
  end loop;  
    
  -- 初始化變量  
  i1 := 1;  
  i3 := '{}';  
  i4 := '{}';  
end loop;  
-- 行號循環  
end;  
$$ language plpgsql strict;  

調用函數計算聚合度

select * from cal_affinity();  
  
NOTICE:  combo: {"(0,1)","(0,2)","(0,3)","(0,4)","(0,5)","(0,6)","(0,7)","(0,8)","(0,9)","(0,10)"}, sum(affinity): 23  
NOTICE:  elements: 7, affinity: 1  
NOTICE:  elements: 9, affinity: 2  
NOTICE:  elements: 3, affinity: 1  
NOTICE:  elements: 1, affinity: 0  
NOTICE:  elements: 4, affinity: 4  
NOTICE:  elements: 6, affinity: 2  
NOTICE:  elements: 8, affinity: 2  
NOTICE:  elements: 5, affinity: 1  
NOTICE:  elements: 2, affinity: 0  
NOTICE:  elements: 10, affinity: 3  
NOTICE:  elements: 0, affinity: 7  
NOTICE:  combo: {"(0,1)","(0,2)","(0,3)","(0,4)","(0,5)","(0,6)","(0,7)","(0,8)","(0,10)","(0,9)"}, sum(affinity): 25  
NOTICE:  elements: 7, affinity: 1  
NOTICE:  elements: 9, affinity: 3  
NOTICE:  elements: 3, affinity: 1  
NOTICE:  elements: 1, affinity: 0  
NOTICE:  elements: 4, affinity: 5  
NOTICE:  elements: 6, affinity: 2  
NOTICE:  elements: 8, affinity: 3  
NOTICE:  elements: 5, affinity: 1  
NOTICE:  elements: 2, affinity: 1  
NOTICE:  elements: 10, affinity: 2  
NOTICE:  elements: 0, affinity: 6  
  
.....  
  

選擇sum(affinity)最小的combo,就是最優的排列組合。將數據按這個順序調整,即可最大的優化性能。

存儲

數據編排是存儲層麵的優化,所以存儲本身的屬性也決定了應該如何進行數據編排。

平麵存儲

平麵存儲,數據按順序在一個或多個平麵上存儲,當從數據庫中順序搜索一批數據時,在物理存儲層麵也可能是相鄰的介質。

所以前麵提到的編排優化是非常有效的。

SSD、3D存儲

硬件特性是SSD或者3D存儲結構時,當從數據庫中順序搜索一批數據時,在物理存儲層麵也可能是相鄰的介質,也可能不是,這個需要根據硬件廠商的寫特性。

前麵的編排對硬件的優化效果可能就沒那麼好。編排算法應該充分考慮硬件的數據訪問特性,才能達到最好的優化效果。

但是對內存的優化效果一定是有的。

GPU - 視覺編排

從前麵的例子來看,數據編排的方法會耗費大量的計算量,10條記錄就可能有300多萬種排列組合,使用CPU運算,計算每種組合的聚合度並不是最好的選擇。

對於此類編排聚合度的計算,視覺處理可能是更好的方法。

pic

小結

1、數據編排的目的和好處:讓數據更加緊湊,降低訪問成本,節約內存。

2、編排應該考慮到多個方麵:

數據的訪問需求,比如是順序訪問,還是隨機訪問,是訪問等值數據,還是訪問範文數據,是訪問標量,還是訪問多值類型、異構類型等。優化是需要針對訪問需求的。

數據存儲的硬件特性,硬件是如何處理數據存儲和搜索的。也決定了應該如何優化。

內存的硬件特性,同上。

軟硬件一體化。

3、PostgreSQL不僅支持簡單的標量類型,還支持複雜的多值類型,例如array, range, json, hstore, gis, xml等。在數據編排優化層麵更加的複雜。

一維類型的編排就好像隻需要完成魔方的一麵

pic

多維類型的編排需要完成所有麵

pic

隨著多維類型元素的增加,記錄數的增加,編排難度也會呈指數上升

pic

多維數據的編排,運算量非常龐大,使用GPU處理可能是一種更好的方法。

PostgreSQL UDF支持pl/cuda編程,可以很好的與GPU進行結合,提高計算能力。

最後更新:2017-06-15 15:31:54

  上一篇:go  優化器裏的概率學 - 性能抖動原理分析
  下一篇:go  一個讓Google、Facebook、Amazon都羨慕的平台,為什麼說阿裏媽媽是數字營銷的未來