支持的版本:当前 (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 可以找出哪种类型的 sum 适用于 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(非数字)输入时选择放弃。

在编写移动聚合支持函数时,务必确保逆向转换函数能够精确地重建正确的状态值。否则,根据是否使用移动聚合模式,结果可能会出现用户可见的差异。一个看似容易添加逆向转换函数,但又无法满足此要求的聚合示例是对 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 级别的等效类型。为了解决这种情况,可以将最终函数声明为接受与聚合的输入参数匹配的额外虚拟参数。由于在调用最终函数时没有可用的特定值,这些虚拟参数始终作为 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)对应于聚合输入的 null 值。与普通聚合一样,finalfunc_extra 仅在聚合是多态的情况下才真正有用;然后需要额外的虚拟参数将最终函数的结果类型连接到聚合的输入类型。

目前,有序集聚合不能用作窗口函数,因此无需支持移动聚合模式。

36.12.4. 部分聚合 #

可选地,聚合函数可以支持部分聚合。部分聚合的想法是独立地对输入数据的不同子集运行聚合的状态转换函数,然后将这些子集产生状态值合并,以产生与在单个操作中扫描所有输入所得的相同状态值。此模式可用于并行聚合,方法是让不同的工作进程扫描表的不同部分。每个工作进程都生成一个部分状态值,最后将这些状态值合并以生成最终状态值。(将来,此模式也可能用于将本地表和远程表的聚合组合,但这尚未实现。)

为了支持部分聚合,聚合定义必须提供一个合并函数,该函数接受聚合状态类型的两个值(表示对输入行的两个子集进行聚合的结果),并产生一个新的状态类型值,该值表示对这些行集组合进行聚合后将得到的状态。未指定来自两个集合的输入行的相对顺序。这意味着通常不可能为对输入行顺序敏感的聚合定义有用的合并函数。

举例来说,通过将合并函数指定为与用作转换函数的两者取大或两者取小比较函数相同,可以使 MAXMIN 聚合支持部分聚合。SUM 聚合只需要一个加法函数作为合并函数。(同样,除非状态值比输入数据类型宽,否则这与其转换函数相同。)

合并函数的处理方式与转换函数非常相似,只是它的第二个参数是状态类型的值,而不是底层输入类型的值。特别是,处理空值和严格函数的规则是类似的。此外,如果聚合定义指定了非空 initcond,请记住,它不仅会用作每个部分聚合运行的初始状态,还会用作合并函数的初始状态,合并函数会被调用以将每个部分结果合并到该状态中。

如果聚合的状态类型声明为 internal,则合并函数有责任确保其结果分配在正确的内存上下文中以存储聚合状态值。 这特别意味着,当第一个输入为 NULL 时,简单地返回第二个输入是无效的,因为该值将处于错误的上下文中,并且没有足够的生命周期。

当聚合的状态类型声明为 internal 时,聚合定义通常也应该提供一个序列化函数和一个反序列化函数,这使得这样的状态值可以从一个进程复制到另一个进程。如果没有这些函数,就无法执行并行聚合,并且未来的应用程序(如本地/远程聚合)也可能无法工作。

序列化函数必须接受一个类型为 internal 的参数,并返回一个类型为 bytea 的结果,该结果表示打包成扁平字节 blob 的状态值。相反,反序列化函数会反转该转换。它必须接受两个类型为 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 中找到示例。

提交更正

如果您发现文档中存在任何不正确、与特定功能的体验不符或需要进一步澄清的地方,请使用 此表单报告文档问题。