1. 程式人生 > 其它 >深入解讀SQL的聚集函式

深入解讀SQL的聚集函式

摘要:本文從基本聚集操作入手,介紹常用的SQL語法,以及一些擴充套件的聚集功能,同時會講到在GaussDB(DWS)裡聚集相關的一些優化思路。

本文分享自華為雲社群《GaussDB(DWS) SQL進階之SQL操作之聚集函式》,作者:兩杯咖啡。

聚集操作是SQL語言中除掃描、投影、連線外的另一個常用基本操作,主要用於對海量資料進行分組,然後在組內進行統計計算的場景。在AP場景下,經常面臨海量資料處理的場景,而終端使用者希望通過海量資料獲取彙總資訊,聚集操作的使用將更加廣泛。本文從基本聚集操作入手,介紹常用的SQL語法,以及一些擴充套件的聚集功能,同時會講到在GaussDB(DWS)裡聚集相關的一些優化思路。

一.典型語法

SQL的聚集操作的典型語法是:

SELECT <column1>, <column2>, Agg_func() FROM t GROUP BY 1, 2 HAVING <filter>;

其中基本元素及概念如下:

  • 聚集操作子句

在SQL中,聚集操作子句通過GROUP BY實現,後面緊接聚集分組列,可以是列名,或者本層輸出列的順序號,從1開始。

  • 聚集分組列

聚集分組列表明本聚集操作是以哪些列的值進行分組的,聚集分組列值均相等的元組會被劃分到同一組。聚集分組列可以是一個,也可以是多個。

  • 聚集函式

聚集函式即進行分組後,每組進行統計計算的函式,分為簡單的和複雜的聚集函式。其中常用簡單聚集函式包括以下五種:

  1. COUNT():用於進行分組內的計數。對於COUNT (column),計數不包含column為NULL值的元組;對於COUNT (*),計數包含所有元組。
  2. SUM():用於計算分組內列或表示式的和,計算不包含列為NULL值的元組。
  3. AVG():用於計算分組內列或表示式的平均值,AVG(col)等價於SUM(col)/ COUNT(col)(分組記憶體在元組)。
  4. MIN():用於計算分組內列或表示式的最小值。
  5. MAX():用於計算分組內列或表示式的最大值。

注:

  1. 如果缺少GROUP BY且包含聚集函式,則所有元組視為一個分組。
  2. 聚集函式不能巢狀。
  • 聚集分組過濾條件

該條件為進行完聚集操作後,以分組為單位進行過濾的條件。聚集分組過濾條件是HAVING條件,在聚集後進行過濾,而我們通常使用的WHERE條件,需要在分組前進行過濾。

語法要求:

由於聚集操作是對聚集列進行去重分組,並進行聚集函式的分組計算,因為聚集操作的輸出列和過濾條件中只能包含聚集列、聚集函式和常量,以及由它們組成的表示式。當出現非聚集列時,查詢會報錯。

特殊地,GaussDB(DWS)支援在主鍵列或唯一約束列上進行聚集的操作(儘管該操作為冗餘操作),此時可以在輸出列和過濾條件中包含任何列。

以TPC-H測試集的lineitem表舉例說明,該表記錄訂單裡的每種型別的零件,所屬的訂單號,零件所屬的供應商,在訂單中的序號以及價格、發貨等資訊。

表定義如下:

CREATE TABLE LINEITEM
(

   L_ORDERKEY    BIGINT NOT NULL
  , L_PARTKEY     BIGINT NOT NULL
  , L_SUPPKEY     BIGINT NOT NULL
  , L_LINENUMBER  BIGINT NOT NULL
  , L_QUANTITY    DECIMAL(15,2) NOT NULL
  , L_EXTENDEDPRICE  DECIMAL(15,2) NOT NULL
  , L_DISCOUNT    DECIMAL(15,2) NOT NULL
  , L_TAX         DECIMAL(15,2) NOT NULL
  , L_RETURNFLAG  CHAR(1) NOT NULL
  , L_LINESTATUS  CHAR(1) NOT NULL
  , L_SHIPDATE    DATE NOT NULL
  , L_COMMITDATE  DATE NOT NULL
  , L_RECEIPTDATE DATE NOT NULL
  , L_SHIPINSTRUCT CHAR(25) NOT NULL
  , L_SHIPMODE     CHAR(10) NOT NULL
  , L_COMMENT      VARCHAR(44) NOT NULL
)
with (orientation = column)
distribute by hash(L_ORDERKEY);
SELECT MAX(l_receiptdate) FROM lineitem; -- 正確,獲得所有零件的最後收貨時間

SELECT SUM(l_quantity) FROM lineitem where l_orderkey=100000; -- 正確,獲得訂單號為100000的零件總數

SELECT l_orderkey, MAX(l_shipdate), MIN(l_shipdate) FROM lineitem GROUP BY l_orderkey; -- 正確,求每個訂單的最早發貨日期和最晚發貨日期

SELECT l_orderkey, MAX(l_shipdate), MIN(l_shipdate) FROM lineitem GROUP BY 1; -- 正確,等價於上一條語句

SELECT l_orderkey, MAX(l_shipdate), MIN(l_shipdate) FROM lineitem GROUP BY 1 HAVING MIN(l_shipdate) < ‘1999-01-01’; -- 正確,求零件最早發貨日期在1999-01-01之前的,每個訂單的最早和最晚的發貨日期(每個零件可能單獨發貨)

SELECT l_orderkey || ‘_’ || SUM(l_quantity), SUM(L_EXTENDEDPRICE) FROM lineitem GROUP BY l_orderkey; -- 正確,求每個訂單的組合標識(訂單號+零件個數),以及總價格

SELECT l_orderkey, l_partkey, AVG(l_discount) FROM lineitem GROUP BY 1; -- 錯誤,l_partkey不是聚集列,但出現在輸出列中

二.GaussDB(DWS)聚集執行及調優

在GaussDB(DWS)中,由於是分散式系統,資料計算應該儘量在各個DN上平行計算以得到最優的效能。因此,支援以下聚集操作計算方式:

  • 如果分佈鍵是GROUP BY列的子集,此時在各個DN上分別計算,結果彙總即可。

例如:lineitem表以l_orderkey作為分佈鍵,則聚集列包含l_orderkey的均可以在各DN執行後彙總。

  • 對於不滿足(1)的場景,各DN分別執行後,DN間仍然可能存在聚集列相等的資料,需要二次聚集,此時GaussDB(DWS)支援三種計算方式。

示例語句(TPC-H Q1,輸出列部分省略):

select
        l_returnflag,
        l_linestatus,
        sum(l_quantity) as sum_qty
from
        lineitem
where
        l_shipdate <= date '1998-12-01' - interval '90' day (3)
group by
        l_returnflag,
        l_linestatus
order by
        l_returnflag,
        l_linestatus;

<1> 各DN上進行一次聚集,將結果彙總到CN上進行二次聚集。

lineitem總共行數為59億行。該方法中,經過DN一次聚集後,各DN輸出4行資料(全域性96行),這些資料彙總到CN上,由CN進行96行資料的二次聚集,最終輸出6行資料。(資料資訊均為估算值)

<2> 選擇聚集列的子集列進行重分佈,回退到(1)的情況後,各DN分別聚集後進行結果彙總。

該方法中,首先按聚集的兩列進行重分佈,重分佈資料量為59億,然後各DN完成聚集,並將結果返回CN。

<3> 各DN上進行一次聚集,然後選擇聚集列的子集列進行重分佈,各DN上進行二次聚集後結果彙總。

該方法中,各DN進行一次聚集,行數由59億減少到4行,然後按聚集的兩列進行重分佈,各DN進行二次聚集。

可以看出,該查詢適合用<1>和<3>的方式進行執行,因為聚集後的行數比較少,在CN上執行或重分佈的資料量都不大,所以開銷較小。而<2>的方式要對59億行資料進行網路重分佈,網路佔用較大。可以總結出三種方法的適用場景:

<1> 該方法適合於一次聚集後行數較少且DN數較少的場景,這樣匯聚到CN的行數較少,不會導致CN成為計算的瓶頸。

<2> 相較於<3>方法,該方法適合於DN一次聚集後行數縮減不明顯的場景,這時可以以所有資料重分佈的代價,省略DN的一次聚集操作。

<3> 與<2>相反,該方法適合於DN一次聚集後行數縮減明顯的場景,例如上面的示例。

在GaussDB(DWS)中,以上三種方法的選擇是根據代價來自動選擇的,也可以通過引數best_agg_plan來強制控制選擇某種方法進行執行。best_agg_plan=1, 2, 3分別對應於上述三種方法,0為預設值,表示由產品自動選擇最優計劃。

在單DN上執行時,GaussDB(DWS)支援以下三種演算法:

<1> Plain Agg:最終僅輸出一行資料,適合於無聚集列的場景。

<2> HashAgg:使用Hash表來進行元組的去重,首先計算聚集列的hash值,hash值相同的再進行列值的比較,避免與所有資料比較後進行去重。去重時進行聚集函式的計算。適合於聚集後行數縮減較多的場景。

<3> Sort + GroupAgg:首先對資料按照聚集列進行排序,這樣聚集列相等的元組均相鄰,通過遍歷一遍排序後的資料,即可完成元組的去重和聚集函式的計算。相較於<2>,適合於聚集後行數縮減較少的場景。

以上<2>和<3>的方法可以通過引數enable_sort和enable_hashagg來控制(預設均為on)。當enable_hashagg=on且enable_sort=off時,優先選擇<2>;當enable_sort=on且enable_hashagg=off時,優先選擇<3>。大資料量場景,通常HashAgg可以獲得較好的效能,所以GaussDB(DWS)對HashAgg進行了較深入的優化。對於個別場景選擇<3>的方法導致效能問題,可以通過關閉enable_sort來進行調優。

三.DISTINCT表示式

聚集函式中,均可以通過關鍵字DISTINCT對聚集列進行去重後進行計算,例如:COUNT(DISTINCT col)表示分組內col值不同的值的個數。

SELECT COUNT(DISTINCT(l_partkey)) FROM lineitem GROUP BY l_returnflag, l_linestatus; -- 計算每種發貨狀態下的不同零件數量

在分散式環境下,為了避免l_partkey相同的值在不同的DN上導致無法去重,GaussDB(DWS)對DISTINCT類操作進行了轉換,上面語句等價於:

SELECT COUNT(l_partkey) FROM (select l_returnflag, l_linestatus, l_partkey FROM lineitem GROUP BY l_returnflag, l_linestatus, l_partkey) GROUP BY l_returnflag, l_linestatus;

這樣,在GaussDB(DWS)中實際上使用兩次Agg來計算DISTINCT表示式的值,計劃如下:

通過計劃可以看出,第8-9層為lineitem基表掃描,上面有兩次Agg處理COUNT(DISTINCT)運算元。第6-7行為第一次Agg,聚集列為:l_returnflag, l_linestatus, l_partkey,選擇Hashagg的方法二;第3-5行為第二次Agg,聚集列為:l_returnflag, l_linestatus,選擇Hashagg的方法三。

注:目前SQL標準僅支援聚集函式中出現一列,對於要求多列的COUNT(DISTINCT),例如:COUNT(DISTINCT l_partkey, l_suppkey),實際可以通過手動使用上述改寫方式進行求解:

SELECT COUNT(1) FROM (select l_returnflag, l_linestatus, l_partkey, l_suppkey FROM lineitem GROUP BY l_returnflag, l_linestatus, l_partkey, l_suppkey) GROUP BY l_returnflag, l_linestatus;

四.聚集擴充套件功能

在SQL 1999標準中,對聚集函式進行了擴充套件,新增了OLAP函式ROLLUP(), CUBE(), GROUPING SETS(),用於更靈活的多維資料分組統計功能。其實,這三個函式都可以使用簡單的GROUP BY的集合合併操作(UNION ALL)來實現,本文中使用UNION ALL(GROUP BY x)來替代,例如:

GROUP BY a UNION ALL GROUP BY b的表示式中,x包括:(a), (b)。本文下面的討論著重針對x進行。

  • ROLLUP()是聚集列字首的聚集結果的合併實現的,例如:

ROLLUP(a, b, c)中,x包括:(a,b,c), (a,b), (a), ()。(其中GROUP BY()表示所有行聚集到一組的無GROUP BY語義),對於n個聚集列,x中包含n+1個聚集組合。

ROLLUP()中的元素可以是列的集合,例如:

ROLLUP((a, b), (b, c)),x包括:(a,b,b,c)(等價於(a,b,c)), (a,b), ()。

  • CUBE()是聚集列組合的列舉的聚集結果合併實現的,例如:

CUBE(a, b, c)中,x包括:(a,b,c), (a,b), (a,c), (b,c), (a), (b), (c), (),對於n個聚集列,x中包含2^n個聚集組合。

  • GROUPING SETS()是聚集列的列舉的聚集結果合併實現的,例如:

GROUPING SETS(a, b, c, d)中,x包括:(a), (b), (c), (d),對於n個聚集列,x中包含n個聚集組合。

由於OLAP函式中,並不是聚集列均出現在每一個聚集結果中,所以增加GROUPING函式來標識引數列是否參與每一行聚集結果的運算,例如:對於CUBE(a, b, c),其中x包括:(a,b,c), (a,b), (a,c), (b,c), (a), (b), (c), ()時,對於x為(a,b,c), (a,b), (a,c), (a)的聚集結果行,GROUPING(a)的值為0,其它為1。

對於包含OLAP函式的如下語句:

select l_returnflag, l_linestatus, l_shipmode, sum(l_extendedprice), grouping(l_returnflag) from lineitem group by cube(1,2,3) order by 1,2,3;

GaussDB(DWS)的計劃如下:

目前GaussDB(DWS)中使用Sort+GroupAgg來實現OLAP函式,後續版本會支援HashAgg進行執行,提高效能。

五.總結

聚集操作是SQL語言中的基本操作,只有深入瞭解聚集操作的語法、語義和支援的功能範圍,才能更靈活地駕馭靈活的SQL語言進行開發,為學習更高階的SQL語言打下良好的基礎。

想了解GuassDB(DWS)更多資訊,歡迎微信搜尋“GaussDB DWS”關注微信公眾號,和您分享最新最全的PB級數倉黑科技,後臺還可獲取眾多學習資料哦~

 

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