2025年9月25日: PostgreSQL 18 釋出!
支援版本:當前 (18) / 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將返回零(初始狀態值)。也許在這種情況下我們希望返回空值——SQL 標準期望sum以這種方式執行。我們可以透過省略initcond短語來做到這一點,這樣初始狀態值就是空值。通常這意味著sfunc需要檢查空狀態值輸入。但是對於sum和一些其他簡單的聚合,如maxmin,將第一個非空輸入值插入狀態變數,然後從第二個非空輸入值開始應用轉換函式就足夠了。PostgreSQL會在初始狀態值為null且轉換函式被標記為嚴格(即,不為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。有序集聚合#

我們到目前為止所描述的聚合都是普通聚合。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。部分聚合#

聚合函式可以選擇支援部分聚合。部分聚合的目的是獨立地在輸入資料的不同子集上執行聚合的狀態轉換函式,然後組合這些子集產生的狀態值,以生成與一次掃描所有輸入相同的結果狀態值。這種模式可以用於並行聚合,讓不同的工作程序掃描表的不同部分。每個工作程序生成一個部分狀態值,最後將這些狀態值組合起來生成一個最終狀態值。(將來,這種模式也可能用於組合本地和遠端表的聚合等目的;但尚未實現。)

為了支援部分聚合,聚合定義必須提供一個組合函式,它接受兩個聚合狀態型別的值(表示對輸入行的兩個子集進行聚合的結果)並生成一個新的狀態型別值,表示聚合這些行集合的組合後狀態會是什麼樣子。兩個集合中輸入行的相對順序是未指定的。這意味著對於對輸入行順序敏感的聚合,通常無法定義有用的組合函式。

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

組合函式被視為與轉換函式非常相似,只不過它的第二個引數是狀態型別的值,而不是底層輸入型別的值。特別是,處理空值和嚴格函式的規則是相似的。此外,如果聚合定義指定了一個非空的 initcond,請記住它不僅將用作每次部分聚合執行的初始狀態,還將用作組合函式的初始狀態,該函式將用於將每個部分結果組合到該狀態中。

如果聚合的狀態型別宣告為internal,則組合函式有責任將其結果分配到聚合狀態值的正確記憶體上下文中。這意味著,特別是當第一個輸入為NULL時,直接返回第二個輸入是無效的,因為該值將在錯誤的上下文中,並且沒有足夠的生命週期。

當聚合的狀態型別宣告為 internal 時,聚合定義通常也需要提供一個序列化函式和一個反序列化函式,它們允許將此類狀態值從一個程序複製到另一個程序。如果沒有這些函式,就無法執行並行聚合,並且未來諸如本地/遠端聚合之類的應用也可能無法工作。

序列化函式必須接受一個 internal 型別的引數並返回一個 bytea 型別的結果,該結果表示打包成扁平位元組塊的狀態值。反之,反序列化函式則逆轉此轉換。它必須接受兩個 byteainternal 型別的引數,並返回一個 internal 型別的結果。(第二個引數未使用且始終為零,但出於型別安全原因需要它。)反序列化函式的結果應僅在當前記憶體上下文中分配,因為與組合函式的結果不同,它不是長壽命的。

還值得注意的是,要並行執行聚合,聚合本身必須標記為 PARALLEL SAFE。其支援函式的並行安全標記不被查閱。

36.12.5。聚合的支援函式#

用 C 編寫的函式可以透過呼叫 AggCheckCallContext 來檢測它是否被用作聚合支援函式,例如

if (AggCheckCallContext(fcinfo, NULL))

檢查此項的一個原因是,當它為真時,第一個輸入必須是臨時狀態值,因此可以安全地就地修改,而無需分配新的副本。請參閱int8inc()以獲取示例。(雖然聚合轉換函式始終允許就地修改轉換值,但通常不鼓勵聚合最終函式這樣做;如果它們這樣做,則在建立聚合時必須宣告其行為。有關更多詳細資訊,請參閱CREATE AGGREGATE。)

AggCheckCallContext 的第二個引數可用於檢索儲存聚合狀態值的記憶體上下文。這對於希望使用擴充套件物件(請參閱第 36.13.1 節)作為其狀態值的轉換函式很有用。在第一次呼叫時,轉換函式應返回一個記憶體上下文是聚合狀態上下文子級的擴充套件物件,然後在後續呼叫中繼續返回相同的擴充套件物件。請參閱array_append()以獲取示例。(array_append()不是任何內建聚合的轉換函式,但它在用作自定義聚合的轉換函式時表現高效。)

C 語言編寫的聚合函式可用的另一個支援例程是 AggGetAggref,它返回定義聚合呼叫的 Aggref 解析節點。這主要對有序集聚合有用,它們可以檢查 Aggref 節點的子結構以找出它們應該實現的排序順序。PostgreSQL 原始碼中的 orderedsetaggs.c 中可以找到示例。

提交更正

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