全部產品
Search
文件中心

PolarDB:偏分析情境的實踐和最佳化

更新時間:Jul 06, 2024

PolarDB-X是一款以TP為主的HTAP資料庫,也支援一定情境的分析需求。而典型的分析情境一般有以下幾類特徵:

  • 少量的寫或者更新要求,大多數是讀請求;

  • 每次查詢都從資料庫中讀取大量的行,但是同時又僅需要少量的列;

  • 大多數查詢都是比較複雜的查詢,查詢的並發不會很大,但單個查詢需要高輸送量;

  • 對於簡單查詢,允許一定的延遲;

  • 分析情境上分散式交易可能不是必須的;

  • 大部分查詢中往往會涉及到事實表和維表的關聯,是典型的大小表關聯情境;

  • 查詢結果明顯小於來源資料,即資料被過濾或彙總後能夠被盛放在單台伺服器的記憶體中;

  • 分析的資料往往是最近的業務資料,歷史資料可以被清理或者被歸檔。

依據上述對分析情境的歸納,分析情境做效能最佳化除了要沿用TP資料庫的最佳化思路,還會有自身不一樣的最佳化思路。主要體現在結構設計查詢最佳化兩個方面。

結構設計

結構設計主要包括如何選擇表類型、分區鍵、主鍵以及聚簇索引,使表的效能達到最優。

設計為分區表或者廣播表

  1. 廣播表會在叢集的每個資料節點都儲存一份資料,建議廣播表的資料量不宜太大,每張廣播表格儲存體的資料不超過20萬行,這樣在大表和廣播表做關聯時,可以計算下推,讓關聯貼近資料層做計算,避免大表資料拉取到計算節點做計算。

  2. 其他業務資料儘可能做成分區表,可以充分利用分布式系統的查詢能力。理論上表的分區數量越多越好,這樣多個分區表可以做並行掃描。儲存層更易做到水平擴充,儲存千萬條甚至上億條資料。實際使用中建議一個分區表的數量在500w~5000w之間。

選擇合適的分區鍵

PolarDB-X預設按照主鍵做分拆,主要為了降低分散式資料庫的使用成本。同時我們也支援通過指定分區鍵建分區表,在分析情境中建議您根據如下依據選擇分區鍵:

  1. 儘可能選擇參與JOIN的欄位作為分區鍵,這樣做的目的是為了關聯條件下推,避免資料被拉取到計算層做計算。

  2. 儘可能選擇值分布均勻的欄位作為分區鍵,這樣可以避免由於分布式不均導致出現計算長尾現象,嚴重拖慢大查詢效能。

合理設計二級分區

PolarDB-X的DRDS模式資料庫支援二級分區。當資料量過大或者有資料扭曲時,二級分區的選擇至關重要,如果資料量大的表中沒有二級分區或者二級分區切分不合理,也會影響效能。如果業務明確有增量資料匯入需求,主要是對最近資料的報表分析,那麼建議用日期格式做二級分區,避免對歷史到期資料的掃描。

//直接用col先做一級分區
PARTITION BY HASH(col) 
SUBPARTITION BY LIST (ds) 
//ds轉換後的月做二級分區
SUBPARTITION TEMPLATE (
   PARTITION p1 VALUES LESS THAN ('2021-08-00'),
   PARTITION p2 VALUES LESS THAN ('2021-09-00'),
)

合理設計索引

如果業務已經按照關聯欄位,合理的設計了分區鍵。但依然還有部分複雜查詢涉及到對該表的其他列做關聯,無法做到關聯查詢下推,此時可以考慮基於該非分區鍵的列做全域二級索引。這樣複雜查詢對該表做關聯,可以轉化成與該全域二級索引做關聯。同時了為了避免回表的代價,對於分析情境建議所有的全域二級索引都建成聚簇索引。

查詢最佳化

在分析情境中,由於會涉及比較大的資料,且對簡單查詢的延遲有一定的容忍度,推薦您採用MPP執行模式,既利用多個計算節點(CN)的計算資源承擔複雜計算。一般只在唯讀執行個體預設開啟MPP能力,如果您可以允許在主執行個體做分析需求,請聯絡阿里雲支援人員。

在查詢過程中,PolarDB-X首先會基於最佳化器選擇合適的分布式執行計畫,然後將計劃調度到各個計算節點,充分發揮整個叢集的計算資源加速查詢。這個過程產生的分布式執行計畫完全是基於統計資訊做代價選擇,因此及時的資訊採集至關重要;同時由於最佳化器產生的計劃不一定是最優的,本文也提供了一些SQL編寫和最佳化的經驗。

收集統計資訊

PolarDB-X會及時定時收集統計資訊,如果發現PolarDB-X產生的分布式執行計畫不是最優的。可以通過ANALYZE TABLE手動對某個表做統計資訊收集。

SQL編寫技巧

  • 去掉不必要的列

    由於分析情境大多數是高吞吐的,所以應該去除返回過程中不必要的列,減少對頻寬的壓力。在編寫SQL時一定要確認業務需要返回的列,不要直接使用星號(*)進行查詢。

    //不合適寫法
    select * from T1 where a1>100 and a2<1000;
    //更合適寫法,只需要返回業務關心的列
    select a1, a2 from T2 where a1>100 and a2<1000;
  • 基於局部索引做過濾

    很多分析情境都期望用時間做二級分區,這樣做巨量資料掃描的時候可以把時間做過濾條件,過濾掉絕大多數歷史資料。

    select a1,c2 from T1 where time >='2010-01-01 00:00:00';

    為了避免全部掃描,目前預設會在這個分區列上做局部索引。同樣的在很多高吞吐的掃描情境下,可以考慮基於過濾條件做局部索引。

  • 避免低效的SQL文法

    如果表記錄數非常大,掃描會很慢,直接導致查詢緩慢。在SQL編寫過程中我們需要注意以下幾點:

    • 避免索引失效

      1. 不在索引列上做任何操作,例如計算、函數、類型轉換(自動或手動),會導致索引失效而轉向全表掃描。

        explain execute select * from staffs where name= 'hu';
        +----+-------------+--------+------------+------+-----------------------+-----------------------+---------+-------+------+----------+-------+
        | id | select_type | table  | partitions | type | possible_keys         | key                   | key_len | ref   | rows | filtered | Extra |
        +----+-------------+--------+------------+------+-----------------------+-----------------------+---------+-------+------+----------+-------+
        |  1 | SIMPLE      | staffs | NULL       | ref  | idx_staffs_nameAgePos | idx_staffs_nameAgePos | 74      | const |    1 |      100 | NULL  |
        +----+-------------+--------+------------+------+-----------------------+-----------------------+---------+-------+------+----------+-------+
        1 row in set , 1 warning (0.00 sec)
        
        //在索引列上做了其他動作,導致索引失效
        explain execute select * from staffs where left(name,4)= 'hu';
        +----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
        | id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
        +----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
        |  1 | SIMPLE      | staffs | NULL       | ALL  | NULL          | NULL | NULL    | NULL |  198 |      100 | Using where |
        +----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
        1 row in set , 1 warning (0.00 sec)
      2. 在使用不等於(!=或<>)的時候,無法使用索引導致全表掃描。

      3. is null,is not null也無法使用索引。

        explain execute select * from staffs where name is null ;
        +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------+
        | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra            |
        +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------+
        |  1 | SIMPLE      | NULL  | NULL       | NULL | NULL          | NULL | NULL    | NULL | NULL | NULL     | Impossible WHERE |
        +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------+
        1 row in set
      4. like以萬用字元開頭,MySQL索引失效會進行全表掃描的操作。

        explain exeucte select * from staffs where name like '%hu' ;
        +----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
        | id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
        +----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
        |  1 | SIMPLE      | staffs | NULL       | ALL  | NULL          | NULL | NULL    | NULL |  198 |    11.11 | Using where |
        +----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------------+
        1 row in set
        
        explain execute select * from staffs where name like 'hu%' ;
        +----+-------------+--------+------------+-------+-----------------------+-----------------------+---------+------+------+----------+-----------------------+
        | id | select_type | table  | partitions | type  | possible_keys         | key                   | key_len | ref  | rows | filtered | Extra                 |
        +----+-------------+--------+------------+-------+-----------------------+-----------------------+---------+------+------+----------+-----------------------+
        |  1 | SIMPLE      | staffs | NULL       | range | idx_staffs_nameAgePos | idx_staffs_nameAgePos | 74      | NULL |    1 |      100 | Using index condition |
        +----+-------------+--------+------------+-------+-----------------------+-----------------------+---------+------+------+----------+-----------------------+
        1 row in set
    • 盡量少用like,like操作一般不會很高效,盡量使用範圍條件到達目的。比如between...and...。

    • 多表關聯情境下:

      1. 盡量包含分區列條件。如果不包含,則盡量通過WHERE條件過濾掉多餘的資料。

      2. outer join的on和where範圍不同。on是作用於join的過程,where是作用於join之後的結果,所以應該將能在join的時候提前過濾的條件寫在on上,也可以寫在join表的子查詢裡,這樣可以減少join原始表的資料量。

資料扭曲的檢查和處理

如果出現查詢異常緩慢,或者資源使用率不均勻的情況,則需要確認是否出現了資料扭曲。一般解決傾斜有三種策略:

  1. 通過show info from table檢查某個分區在各個節點上的資料分布情況,如果各節點上的資料分布明顯不均勻,則可以考慮對該表的分區鍵進行調整。

  2. 如果是出現了嚴重Join Key熱點問題,將傾斜的Key用單獨的邏輯來處理。例如兩邊的Key中有大量NULL資料導致了傾斜,則需要在Join前先過濾掉NULL資料或者補上隨機數,然後再進行Join,樣本如下。

    SELECT * FROM A JOIN B ON CASE WHEN A.value IS NULL THEN CONCAT('value',RAND() ) ELSE A.value END = B.value;

    在實際情境中,如果您發現已經資料扭曲,但無法擷取導致資料扭曲的Key資訊,可以使用如下方法查看資料扭曲。

    --執行如下語句查詢資料扭曲。
    SELECT * FROM a JOIN b ON a.key=b.key;  
    --您可以執行如下SQL,查看Key的分布,判斷執行Join操作時是否會有資料扭曲。
    SELECT left.key, left.cnt * right.cnt FROM 
    (select key, count(*) AS cnt FROM a GROUP BY key) LEFT 
    JOIN
    (SELECT key, COUNT(*) AS cnt FROM b GROUP BY key) RIGHT
    ON left.key=right.key;
  3. 如果Group By Key出現了熱點問題,可以考慮對SQL進行改寫,添加隨機數,把長Key進行拆分。例如:

    SELECT Key,COUNT(*) AS Cnt FROM TableName GROUP BY Key;
    //最佳化成以下SQL,先對熱點做打散預彙總,再做最終彙總
    -- 假設長尾的Key已經找到是KEY001。
    SELECT a.Key
      , SUM(a.Cnt) AS Cnt
    FROM (
      SELECT Key
        , COUNT(*) AS Cnt
    FROM TableName
    GROUP BY Key, 
      CASE 
        WHEN Key = 'KEY001' THEN rand() % 50
        ELSE 0
       END
    ) a
    GROUP BY a.Key;

調整執行策略

按照上述策略調整後,查詢效能依然不理想且計算和儲存資源都未到達瓶頸,這個時候可以調整下執行策略。主要有兩種方式去調整:

  1. 加大並發度,您可以通過 /*+TDDL:MPP_PARALLELISM=4*/ 指定MPP執行器並行度。

    /*+TDDL:MPP_PARALLELISM=4*/ select a.k, count(*) cnt from sbtest1 a, sbtest1 b where a.id = b.k and a.id > 1000 group by k having cnt > 1300 or
    der by cnt limit 5, 10;
  2. 通過HINT指定特定的演算法,如何調整更好的彙總演算法和關聯演算法,請參見彙總關聯