支援的版本:目前 (17) / 16 / 15 / 14 / 13
開發版本:devel
不支援的版本:12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0 / 7.4 / 7.3 / 7.2 / 7.1

36.12. 使用者定義的彙總 #

PostgreSQL 中的彙總函數是根據狀態值狀態轉換函數定義的。也就是說,彙總會使用一個狀態值來操作,該狀態值會在處理每個連續的輸入列時更新。要定義一個新的彙總函數,需要選擇狀態值的資料類型、狀態的初始值,以及狀態轉換函數。狀態轉換函數會接收先前的狀態值和目前列的彙總輸入值,並傳回新的狀態值。最終函數也可以指定,以防彙總的所需結果與需要在執行狀態值中保存的資料不同。最終函數會接收結束狀態值,並傳回任何所需的彙總結果。原則上,轉換函數和最終函數只是普通的函數,也可以在彙總的上下文之外使用。(實際上,出於效能考量,建立只能在作為彙總的一部分呼叫時才能運作的特殊轉換函數通常很有幫助。)

因此,除了彙總的使用者看到的引數和結果資料類型之外,還有一個內部狀態值資料類型,它可能與引數類型和結果類型都不同。

如果我們定義一個不使用最終函數的彙總,我們就會得到一個計算來自每列值的執行函數的彙總。sum 就是這類彙總的一個範例。sum 從零開始,並且總是將目前列的值新增到其執行總計。例如,如果我們想要建立一個可以在複數資料類型上運作的 sum 彙總,我們只需要該資料類型的加法函數。彙總定義將是

CREATE AGGREGATE sum (complex)
(
    sfunc = complex_add,
    stype = complex,
    initcond = '(0,0)'
);

我們可以像這樣使用它

SELECT sum(a) FROM test_complex;

   sum
-----------
 (34,53.9)

(請注意,我們依賴於函數多載:有多個名為 sum 的彙總,但 PostgreSQL 可以找出哪種總和適用於 complex 類型的列。)

如果沒有非空輸入值,上述 sum 的定義將傳回零(初始狀態值)。也許我們希望在這種情況下傳回 null — SQL 標準希望 sum 以這種方式運作。我們可以簡單地省略 initcond 片語來做到這一點,以便初始狀態值為 null。通常,這意味著 sfunc 需要檢查是否有 null 狀態值輸入。但是對於 sum 和其他一些簡單的彙總(如 maxmin),將第一個非空輸入值插入到狀態變數中,然後從第二個非空輸入值開始套用轉換函數就足夠了。如果初始狀態值為 null,並且轉換函數標記為strict(也就是說,不為 null 輸入呼叫),PostgreSQL 將自動執行此操作。

strict轉換函數的另一個預設行為是,每當遇到 null 輸入值時,先前的狀態值都會保持不變。因此,null 值會被忽略。如果您需要 null 輸入的其他一些行為,請不要將您的轉換函數宣告為 strict;而是編碼它來測試 null 輸入,並執行任何需要的操作。

avg(平均值)是一個更複雜的彙總範例。它需要兩個執行狀態:輸入的總和和輸入數量的計數。最終結果是透過將這些量相除獲得的。平均值通常透過使用陣列作為狀態值來實作。例如,avg(float8) 的內建模組實作如下所示

CREATE AGGREGATE avg (float8)
(
    sfunc = float8_accum,
    stype = float8[],
    finalfunc = float8_avg,
    initcond = '{0,0,0}'
);

注意

float8_accum 需要一個包含三個元素的陣列,而不僅僅是兩個元素,因為它會累加平方和以及輸入的總和和計數。這是為了使它可以同時用於其他一些彙總以及 avg

SQL 中的彙總函數呼叫允許 DISTINCTORDER BY 選項,這些選項控制哪些列被提供給彙總的轉換函數以及以什麼順序提供。這些選項在幕後實作,並且與彙總的支援函數無關。

如需更多詳細資訊,請參閱CREATE AGGREGATE 命令。

36.12.1. 移動彙總模式 #

彙總函式可以選擇性地支援移動彙總模式,這允許在具有移動框架起點的視窗中,大幅加快彙總函式的執行速度。(關於將彙總函式用作視窗函式的資訊,請參閱第 3.5 節第 4.2.8 節。)其基本概念是,除了正常的向前轉換函式之外,彙總函式還提供一個反向轉換函式,這允許在列離開視窗框架時,從彙總函式的執行狀態值中移除列。例如,使用加法作為向前轉換函式的 sum 彙總函式,將使用減法作為反向轉換函式。如果沒有反向轉換函式,視窗函式機制必須在每次框架起點移動時從頭開始重新計算彙總,導致執行時間與輸入列的數量乘以平均框架長度成正比。有了反向轉換函式,執行時間僅與輸入列的數量成正比。

反向轉換函式會被傳遞目前狀態值以及目前狀態中包含的最早列的彙總輸入值。它必須重建如果給定的輸入列從未被彙總,但僅彙總其後的列,狀態值應該是什麼。這有時要求向前轉換函式保留比普通彙總模式所需的更多狀態。因此,移動彙總模式使用與普通模式完全分離的實作:它有自己的狀態資料類型、自己的向前轉換函式,以及如果需要,自己的最終函式。如果不需要額外的狀態,這些可以與普通模式的資料類型和函式相同。

舉例來說,我們可以擴展上面給出的 sum 彙總函式,以支援像這樣的移動彙總模式

CREATE AGGREGATE sum (complex)
(
    sfunc = complex_add,
    stype = complex,
    initcond = '(0,0)',
    msfunc = complex_add,
    minvfunc = complex_sub,
    mstype = complex,
    minitcond = '(0,0)'
);

名稱以 m 開頭的參數定義了移動彙總實作。除了反向轉換函式 minvfunc 之外,它們對應於沒有 m 的普通彙總參數。

不允許移動彙總模式的向前轉換函式傳回 null 作為新的狀態值。如果反向轉換函式傳回 null,則表示反向函式無法反轉此特定輸入的狀態計算,因此將從頭開始重新計算目前框架起點的彙總計算。此約定允許在某些不常發生的情況下使用移動彙總模式,這些情況不切實際地從執行狀態值中反轉出來。反向轉換函式可以在這些情況下放棄,但只要它可以在大多數情況下工作,仍然可以領先。例如,處理浮點數的彙總可能會選擇在必須從執行狀態值中移除 NaN(非數字)輸入時放棄。

在編寫移動彙總支援函式時,務必確保反向轉換函式可以準確地重建正確的狀態值。否則,根據是否使用移動彙總模式,結果可能會出現使用者可見的差異。一個最初添加反向轉換函式似乎很容易,但無法滿足此要求的彙總函式範例是 sumfloat4float8 輸入上的彙總。一個天真的 sum(float8) 宣告可能是

CREATE AGGREGATE unsafe_sum (float8)
(
    stype = float8,
    sfunc = float8pl,
    mstype = float8,
    msfunc = float8pl,
    minvfunc = float8mi
);

但是,此彙總函式的結果可能與沒有反向轉換函式時的結果有很大差異。例如,考慮

SELECT
  unsafe_sum(x) OVER (ORDER BY n ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING)
FROM (VALUES (1, 1.0e20::float8),
             (2, 1.0::float8)) AS v (n,x);

此查詢將傳回 0 作為其第二個結果,而不是預期的 1。原因是浮點值的有限精度:將 1 加到 1e20 會再次得到 1e20,因此從中減去 1e20 會產生 0,而不是 1。請注意,這是浮點算術的一般限制,而不是 PostgreSQL 的限制。

36.12.2. 多型和可變彙總函式 #

彙總函式可以使用多型狀態轉換函式或最終函式,以便可以使用相同的函式來實作多個彙總函式。有關多型函式的說明,請參閱第 36.2.5 節。更進一步,可以使用多型輸入類型和狀態類型來指定彙總函式本身,允許單個彙總定義服務於多個輸入資料類型。這是一個多型彙總的範例

CREATE AGGREGATE array_accum (anycompatible)
(
    sfunc = array_append,
    stype = anycompatiblearray,
    initcond = '{}'
);

在這裡,任何給定彙總呼叫的實際狀態類型都是具有實際輸入類型作為元素陣列類型。彙總的行為是將所有輸入連接到該類型的陣列中。(注意:內建彙總 array_agg 提供了類似的功能,其效能比此定義更好。)

這是使用兩種不同的實際資料類型作為引數的輸出

SELECT attrelid::regclass, array_accum(attname)
    FROM pg_attribute
    WHERE attnum > 0 AND attrelid = 'pg_tablespace'::regclass
    GROUP BY attrelid;

   attrelid    |              array_accum
---------------+---------------------------------------
 pg_tablespace | {spcname,spcowner,spcacl,spcoptions}
(1 row)

SELECT attrelid::regclass, array_accum(atttypid::regtype)
    FROM pg_attribute
    WHERE attnum > 0 AND attrelid = 'pg_tablespace'::regclass
    GROUP BY attrelid;

   attrelid    |        array_accum
---------------+---------------------------
 pg_tablespace | {name,oid,aclitem[],text[]}
(1 row)

通常,具有多型結果類型的彙總函式具有多型狀態類型,如上例所示。這是必要的,因為否則無法合理地宣告最終函式:它需要具有多型結果類型,但沒有多型引數類型,CREATE FUNCTION 將會拒絕,理由是無法從呼叫中推斷出結果類型。但有時使用多型狀態類型是不方便的。最常見的情況是彙總支援函式將以 C 語言編寫,並且狀態類型應宣告為 internal,因為沒有 SQL 層級的等效項。為了處理這種情況,可以將最終函式宣告為採用與彙總輸入引數匹配的額外虛擬引數。由於在呼叫最終函式時沒有可用的特定值,因此始終將此類虛擬引數作為 null 值傳遞。它們的唯一用途是允許將多型最終函式的結果類型連接到彙總的輸入類型。例如,內建彙總 array_agg 的定義等效於

CREATE FUNCTION array_agg_transfn(internal, anynonarray)
  RETURNS internal ...;
CREATE FUNCTION array_agg_finalfn(internal, anynonarray)
  RETURNS anyarray ...;

CREATE AGGREGATE array_agg (anynonarray)
(
    sfunc = array_agg_transfn,
    stype = internal,
    finalfunc = array_agg_finalfn,
    finalfunc_extra
);

在這裡,finalfunc_extra 選項指定最終函式除了狀態值之外,還會收到與彙總輸入引數相對應的額外虛擬引數。額外的 anynonarray 引數允許 array_agg_finalfn 的宣告有效。

通過將其最後一個引數宣告為 VARIADIC 陣列,可以使彙總函式接受數量可變的引數,這與常規函式非常相似;請參閱第 36.5.6 節。彙總的轉換函式必須具有與其最後一個引數相同的陣列類型。轉換函式通常也應該標記為 VARIADIC,但這不是嚴格要求的。

注意

在與 ORDER BY 選項結合使用時,可變彙總很容易被誤用(請參閱第 4.2.7 節),因為解析器無法判斷在這種組合中是否給出了錯誤數量的實際引數。請記住,ORDER BY 右側的所有內容都是排序鍵,而不是彙總的引數。例如,在

SELECT myaggregate(a ORDER BY a, b, c) FROM ...

解析器會將其視為單個彙總函式引數和三個排序鍵。但是,使用者可能打算

SELECT myaggregate(a, b, c ORDER BY a) FROM ...

如果 myaggregate 是可變的,則這兩個呼叫都可能是完全有效的。

由於同樣的原因,在建立具有相同名稱和不同數量常規引數的彙總函式之前,最好三思而後行。

36.12.3. 排序集合聚集 (Ordered-Set Aggregates) #

到目前為止,我們所描述的聚集是一般聚集。PostgreSQL也支援排序集合聚集,它與一般聚集在兩個關鍵方面有所不同。首先,除了每個輸入列評估一次的普通聚集參數外,排序集合聚集可以具有直接參數,這些參數僅在每次聚集操作中評估一次。其次,普通聚集參數的語法明確指定了它們的排序。排序集合聚集通常用於實現依賴於特定列排序的計算,例如等級或百分位數,因此排序是任何呼叫的必要方面。例如,percentile_disc的內建定義等同於

CREATE FUNCTION ordered_set_transition(internal, anyelement)
  RETURNS internal ...;
CREATE FUNCTION percentile_disc_final(internal, float8, anyelement)
  RETURNS anyelement ...;

CREATE AGGREGATE percentile_disc (float8 ORDER BY anyelement)
(
    sfunc = ordered_set_transition,
    stype = internal,
    finalfunc = percentile_disc_final,
    finalfunc_extra
);

這個聚集接受一個float8直接參數(百分位數比例)和一個可以是任何可排序資料類型的聚集輸入。它可以像這樣用於取得家庭收入中位數

SELECT percentile_disc(0.5) WITHIN GROUP (ORDER BY income) FROM households;
 percentile_disc
-----------------
           50489

在這裡,0.5是一個直接參數;對於跨列變化的百分位數比例沒有意義。

與一般聚集的情況不同,排序集合聚集的輸入列排序不是在幕後完成的,而是由聚集的支援函數負責。典型的實作方法是在聚集的狀態值中保留對tuplesort物件的引用,將傳入的列饋送到該物件中,然後完成排序並在最終函數中讀取資料。這種設計允許最終函數執行特殊操作,例如將額外的假設性列注入到要排序的資料中。雖然一般聚集通常可以使用以PL/pgSQL或其他 PL 語言編寫的支援函數來實作,但排序集合聚集通常必須以 C 語言編寫,因為它們的狀態值不能定義為任何 SQL 資料類型。(在上面的例子中,請注意狀態值被宣告為internal類型 — 這是典型的。)此外,由於最終函數執行排序,因此無法透過稍後再次執行轉換函數來繼續新增輸入列。這意味著最終函數不是READ_ONLY;它必須在CREATE AGGREGATE中宣告為READ_WRITE,或者如果額外的最終函數呼叫可以利用已經排序的狀態,則宣告為SHAREABLE

排序集合聚集的狀態轉換函數接收當前狀態值以及每列的聚集輸入值,並傳回更新後的狀態值。這與一般聚集的定義相同,但請注意,不提供直接參數(如果有的話)。最終函數接收最後的狀態值,直接參數的值(如果有的話),以及(如果指定了finalfunc_extra)對應於聚集輸入的空值。與一般聚集一樣,只有當聚集是多型的時,finalfunc_extra才真正有用;然後需要額外的虛擬參數來將最終函數的結果類型連接到聚集的輸入類型。

目前,排序集合聚集不能用作視窗函數,因此它們無需支援移動聚集模式。

36.12.4. 部分聚集 (Partial Aggregation) #

選擇性地,聚集函數可以支援部分聚集。部分聚集的想法是獨立地在輸入資料的不同子集上執行聚集的狀態轉換函數,然後合併來自這些子集的狀態值,以產生與單次操作中掃描所有輸入所產生的相同狀態值。此模式可用於並行聚集,方法是讓不同的工作進程掃描表格的不同部分。每個工作進程產生一個部分狀態值,最後將這些狀態值合併以產生最終狀態值。(將來,此模式也可能用於組合本機和遠端表格上的聚集等目的;但尚未實作。)

為了支援部分聚集,聚集定義必須提供一個合併函數,它接受聚集的狀態類型的兩個值(代表聚集在輸入列的兩個子集上的結果)並產生狀態類型的新值,代表在這些列集合的組合上聚集後的狀態。未指定來自兩個集合的輸入列的相對順序。這意味著通常不可能為對輸入列順序敏感的聚集定義有用的合併函數。

作為簡單的例子,可以透過將合併函數指定為與用作其轉換函數的相同的兩個比較函數中較大或較小的函數,來使MAXMIN聚集支援部分聚集。SUM聚集只需要一個加法函數作為合併函數。(同樣,這與它們的轉換函數相同,除非狀態值比輸入資料類型寬。)

合併函數的處理方式很像轉換函數,恰好採用狀態類型的值,而不是基礎輸入類型的值作為其第二個參數。特別是,處理空值和嚴格函數的規則類似。此外,如果聚集定義指定了一個非空initcond,請記住,這不僅將用作每個部分聚集運行的初始狀態,還將用作合併函數的初始狀態,該函數將被呼叫以將每個部分結果合併到該狀態中。

如果聚集的狀態類型宣告為internal,則合併函數有責任將其結果分配在聚集狀態值的正確記憶體內容中。這尤其意味著當第一個輸入為NULL時,簡單地傳回第二個輸入是無效的,因為該值將位於錯誤的內容中並且沒有足夠的生命週期。

當聚集的狀態類型宣告為 internal 時,通常也適合在聚集定義中提供序列化函式反序列化函式,以便將此狀態值從一個程序複製到另一個程序。如果沒有這些函式,則無法執行平行聚集,並且諸如本地/遠端聚集之類的未來應用程式可能也無法運作。

序列化函式必須接受一個 internal 類型的單一引數,並傳回一個 bytea 類型的结果,它表示打包成平坦位元組 blob 的狀態值。相反地,反序列化函式會反轉該轉換。它必須接受兩個分別為 byteainternal 類型的引數,並傳回一個 internal 類型的结果。(第二個引數未使用且始終為零,但由於類型安全原因需要它。)反序列化函式的結果應僅在目前的記憶體內容中分配,因為與 combine 函式的結果不同,它不是長期存在的。

還值得注意的是,為了平行執行聚集,聚集本身必須標記為 PARALLEL SAFE。不會查詢其支援函式的平行安全標記。

36.12.5. 聚集的支援函式 #

用 C 撰寫的函式可以透過呼叫 AggCheckCallContext 來偵測它是否正在作為聚集支援函式被呼叫,例如

if (AggCheckCallContext(fcinfo, NULL))

檢查這一點的一個原因是,當它為真時,第一個輸入必須是一個暫時狀態值,因此可以安全地就地修改,而不是分配一個新副本。請參閱 int8inc() 以取得範例。(雖然總是允許聚集轉換函式就地修改轉換值,但不建議聚集最終函式這樣做;如果它們這樣做,則必須在建立聚集時宣告該行為。有關更多詳細資訊,請參閱 CREATE AGGREGATE。)

AggCheckCallContext 的第二個引數可用於檢索保持聚集狀態值的記憶體內容。這對於希望使用 擴充 物件(請參閱 Section 36.13.1)作為其狀態值的轉換函式很有用。在第一次呼叫時,轉換函式應傳回一個擴充物件,其記憶體內容是聚集狀態內容的子項,然後在後續呼叫中保持傳回相同的擴充物件。請參閱 array_append() 以取得範例。(array_append() 不是任何內建聚集的轉換函式,但它的編寫方式使其在用作自訂聚集的轉換函式時能有效率地運作。)

C 語言編寫的聚集函式可用的另一個支援常式是 AggGetAggref,它傳回定義聚集呼叫的 Aggref 剖析節點。這主要對於有序集合聚集很有用,它們可以檢查 Aggref 節點的子結構,以找出它們應該實作哪種類型的排序。可以在 PostgreSQL 原始碼中的 orderedsetaggs.c 中找到範例。

提交更正

如果您在文件中發現任何不正確、與您對特定功能的體驗不符或需要進一步澄清的地方,請使用 此表單 報告文件問題。