PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

36.12. 用户定义的聚集函数 #

36.12.1. 移动聚集模式
36.12.2. 多态与可变参数聚集
36.12.3. 有序集聚合
36.12.4. 部分聚集
36.12.5. 聚集的支持函数

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 能判断出哪一种 sum 适用于 complex 类型的列。)

上面 sum 的定义在没有非空输入值时会返回零(即 初始状态值)。也许我们希望在这种情况下返回空值 — SQL 标准就 要求 sum 这样表现。要做到这一点,只需省略 initcond 短语,让初始状态值为空即可。通常这意味着 sfunc 必须检查空的状态值输入。但对于 sum 以及 max、 min 等其他一些简单聚集,只要把第一个非空输入值 插入状态变量,然后从第二个非空输入值开始应用转换函数就足够了。 PostgreSQL 会在初始状态值为空且转换函数被 标记为“严格”(即不对空输入调用)时自动这样做。

“严格”转换函数的另一项默认行为是,只要遇到空输入值, 前一个状态值就原样保留。因此空值会被忽略。如果需要对空输入采用其他 行为,就不要把转换函数声明为严格的,而是编写它来检查空输入并做任何 需要的处理。

avg(平均值)是更复杂的聚集示例。它需要两个运行 状态:输入的总和与输入的个数。最终结果由这两个量相除得到。平均值 通常用一个数组作为状态值来实现。例如,内建的 avg(float8) 实现看起来是:

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

Note

float8_accum 需要一个三元素数组而不只是两个元素, 因为它除了输入的总和与个数之外,还要累积平方和。这样它还可以用于 avg 之外的其他聚集。

SQL 中的聚集函数调用允许 DISTINCT 和 ORDER BY 选项,用于控制哪些行以什么顺序被送入聚集 的转换函数。这些选项在幕后实现,与聚集的支持函数无关。

更多细节请参见 CREATE AGGREGATE 命令。

36.12.1. 移动聚集模式 #

聚集函数可以可选地支持移动聚集模式,它能显著 加速在移动帧起点的窗口内执行的聚集函数。 (关于把聚集函数用作窗口函数,请参见 Section 3.5 和 Section 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 的普通聚集参数一一对应。

移动聚集模式的正向转换函数不允许返回空值作为新的状态值。如果逆转换 函数返回空值,则表示逆函数无法对这一特定输入逆转状态计算,于是当前 帧起始位置的聚集计算会从头重做。这一约定使移动聚集模式也能用于那些 存在少量难以从运行状态值中逆转的情形的场景。逆转换函数可以在这些情况 下“放弃”,只要它对大多数情况都能奏效,总体上仍然是划算 的。例如,处理浮点数的聚集在需要把一个 NaN(非 数值)输入从运行状态值中移除时,可能会选择放弃。

编写移动聚集支持函数时,务必确保逆转换函数能够精确地重建正确的状态 值。否则,是否使用移动聚集模式可能导致用户可见的结果差异。 对 float4 或 float8 输入的 sum 就是一个乍看起来容易添加逆转换函数、实则无法 满足这一要求的聚集例子。一个朴素的 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. 多态与可变参数聚集 #

聚集函数可以使用多态的状态转换函数或最终函数,从而使同一组函数能够 实现多个聚集。 多态函数的解释请参见 Section 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 层面的等价物)。为应对这种 情况,可以把最终函数声明为接收与聚集输入参数匹配的额外 “哑”参数。这些哑参数总是以空值传入,因为最终函数被调用 时并没有具体的值可用。它们唯一的用途是让多态最终函数的结果类型能够 与聚集的输入类型关联起来。例如,内建聚集 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 数组,可以让 聚集函数接受可变数量的参数,方式与普通函数基本相同;参见 Section 36.5.6。聚集的转换函数的 最后一个参数必须具有相同的数组类型。转换函数通常也会被标记为 VARIADIC,但这并非严格必需。

Note

可变参数聚集在与 ORDER BY 选项(参见 Section 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. 部分聚集 #

聚集函数可以可选地支持部分聚集。部分聚集的 思路是:对输入数据的不同子集独立地运行聚集的状态转换函数,然后把 这些子集得到的状态值合并起来,产生与一次扫描全部输入所得相同的状态 值。这种模式可用于并行聚集:让不同的工作进程扫描表的不同部分。每个 工作进程产生一个部分状态值,最后把这些状态值合并成最终的状态值。 (未来这种模式还可能用于合并对本地表和远程表的聚集等用途;但目前 尚未实现。)

要支持部分聚集,聚集定义必须提供一个合并函数, 它接收聚集状态类型的两个值(分别代表对两个输入行子集聚合的结果), 并产生状态类型的一个新值,代表对这两组行的并集聚合后的状态。两组 输入行的相对顺序是不确定的。这意味着对于对输入行顺序敏感的聚集, 通常不可能定义出有用的合并函数。

作为简单的例子,MAX 和 MIN 聚集只需把合并函数指定为与它们的转换函数相同的取二者较大/较小的 比较函数,即可支持部分聚集。SUM 聚集只需要一个 加法函数作为合并函数。(同样,这与它们的转换函数相同,除非状态值比 输入数据类型更宽。)

合并函数的处理方式很像一个碰巧以状态类型(而非底层输入类型)的值 作为第二个参数的转换函数。特别地,处理空值和严格函数的规则是类似的。 另外,如果聚集定义指定了非空的 initcond,请记住它 不仅会作为每次部分聚集运行的初始状态,也会作为合并函数的初始状态, 每个部分结果都会被合并进该状态。

如果聚集的状态类型被声明为 internal,那么合并函数必须 负责让它的结果被分配在适用于聚集状态值的内存上下文中。特别地,当 第一个输入为 NULL 时,简单地返回第二个输入是无效 的,因为那个值位于错误的上下文中,而且寿命不够长。

当聚集的状态类型被声明为 internal 时,聚集定义通常还应 提供一个序列化函数和一个 反序列化函数,使这种状态值能够从一个进程复制 到另一个进程。没有这些函数,就无法执行并行聚集,未来诸如本地/远程 聚集之类的应用也无法工作。

序列化函数必须接受一个 internal 类型的参数,返回一个 bytea 类型的结果,表示被打包成扁平字节块的状态值。 反之,反序列化函数逆转这一转换。它必须接受 bytea 和 internal 两个类型的参数,返回 internal 类型的结果。(第二个参数未被使用且总是零,但出于类型安全的考虑它是 必需的。)反序列化函数的结果应直接在当前内存上下文中分配,因为与 合并函数的结果不同,它不是长期存活的。

另外值得注意的是,要让聚集以并行方式执行,聚集本身必须被标记为 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 中找到。