2024年9月26日:PostgreSQL 17 发布!
支持的版本:当前 (17) / 16 / 15 / 14 / 13 / 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 短语来实现这一点,以便初始状态值为 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 节。)基本思想是,除了正常的forward 转换函数外,聚合函数还提供了一个逆转换函数,该函数允许在行退出窗口帧时将其从聚合函数的运行状态值中删除。例如,一个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(非数字)输入时放弃。

编写移动聚合支持函数时,务必确保逆转换函数能够精确地重构正确的状态值。否则,根据是否使用移动聚合模式,结果可能会出现用户可见的差异。一个聚合函数的例子,乍一看添加逆转换函数似乎很容易,但无法满足此要求,那就是对float4float8 输入的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. 多态和可变参数聚合函数 #

聚合函数可以使用多态状态转换函数或最终函数,以便相同的函数可用于实现多个聚合。有关多态函数的说明,请参阅第 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数组来使其接受可变数量的参数,这与常规函数的方式非常相似;请参阅第 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 中找到。

提交更正

如果您在文档中看到任何不正确的内容,与您对特定功能的体验不符或需要进一步说明的内容,请使用 此表单 报告文档问题。