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
和一些其他简单的聚合,如max
和min
,将第一个非空输入值插入状态变量,然后从第二个非空输入值开始应用转换函数就足够了。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 中的聚合函数调用允许 DISTINCT
和 ORDER BY
选项,它们控制哪些行被馈送到聚合的转换函数以及以什么顺序。这些选项在后台实现,与聚合的支持函数无关。
有关更多详细信息,请参阅CREATE AGGREGATE 命令。
聚合函数可选择支持移动聚合模式,这允许在窗口内以移动帧起始点更快地执行聚合函数。(有关将聚合函数用作窗口函数的信息,请参见第 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
(非数字)输入时可能会选择跳过。
编写移动聚合支持函数时,务必确保逆向转换函数能够精确地重建正确的状态值。否则,根据是否使用移动聚合模式,结果可能会出现用户可见的差异。一个例子是 sum
在 float4
或 float8
输入上添加逆向转换函数似乎很容易,但无法满足此要求的聚合。一个天真的 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.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
是可变参数的,那么这两个调用都可能完全有效。
出于同样的原因,在创建具有相同名称和不同数量的常规参数的聚合函数之前,最好三思。
我们到目前为止所描述的聚合都是“普通”聚合。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
仅在聚合是多态时才真正有用;在这种情况下,需要额外的虚拟参数来将最终函数的结果类型连接到聚合的输入类型。
目前,有序集聚合不能用作窗口函数,因此它们不需要支持移动聚合模式。
聚合函数可以选择支持部分聚合。部分聚合的目的是独立地在输入数据的不同子集上运行聚合的状态转换函数,然后组合这些子集产生的状态值,以生成与一次扫描所有输入相同的结果状态值。这种模式可以用于并行聚合,让不同的工作进程扫描表的不同部分。每个工作进程生成一个部分状态值,最后将这些状态值组合起来生成一个最终状态值。(将来,这种模式也可能用于组合本地和远程表的聚合等目的;但尚未实现。)
为了支持部分聚合,聚合定义必须提供一个组合函数,它接受两个聚合状态类型的值(表示对输入行的两个子集进行聚合的结果)并生成一个新的状态类型值,表示聚合这些行集合的组合后状态会是什么样子。两个集合中输入行的相对顺序是未指定的。这意味着对于对输入行顺序敏感的聚合,通常无法定义有用的组合函数。
作为简单的例子,MAX
和 MIN
聚合可以通过将组合函数指定为与它们的转换函数相同的两个值中取较大值或较小值的比较函数来支持部分聚合。SUM
聚合只需要一个加法函数作为组合函数。(同样,这与它们的转换函数相同,除非状态值比输入数据类型更宽。)
组合函数被视为与转换函数非常相似,只不过它的第二个参数是状态类型的值,而不是底层输入类型的值。特别是,处理空值和严格函数的规则是相似的。此外,如果聚合定义指定了一个非空的 initcond
,请记住它不仅将用作每次部分聚合运行的初始状态,还将用作组合函数的初始状态,该函数将用于将每个部分结果组合到该状态中。
如果聚合的状态类型声明为internal
,则组合函数有责任将其结果分配到聚合状态值的正确内存上下文中。这意味着,特别是当第一个输入为NULL
时,直接返回第二个输入是无效的,因为该值将在错误的上下文中,并且没有足够的生命周期。
当聚合的状态类型声明为 internal
时,聚合定义通常也需要提供一个序列化函数和一个反序列化函数,它们允许将此类状态值从一个进程复制到另一个进程。如果没有这些函数,就无法执行并行聚合,并且未来诸如本地/远程聚合之类的应用也可能无法工作。
序列化函数必须接受一个 internal
类型的参数并返回一个 bytea
类型的结果,该结果表示打包成扁平字节块的状态值。反之,反序列化函数则逆转此转换。它必须接受两个 bytea
和 internal
类型的参数,并返回一个 internal
类型的结果。(第二个参数未使用且始终为零,但出于类型安全原因需要它。)反序列化函数的结果应仅在当前内存上下文中分配,因为与组合函数的结果不同,它不是长寿命的。
还值得注意的是,要并行执行聚合,聚合本身必须标记为 PARALLEL SAFE
。其支持函数的并行安全标记不被查阅。
用 C 编写的函数可以通过调用 AggCheckCallContext
来检测它是否被用作聚合支持函数,例如
if (AggCheckCallContext(fcinfo, NULL))
检查此项的一个原因是,当它为真时,第一个输入必须是临时状态值,因此可以安全地就地修改,而无需分配新的副本。请参阅int8inc()
以获取示例。(虽然聚合转换函数始终允许就地修改转换值,但通常不鼓励聚合最终函数这样做;如果它们这样做,则在创建聚合时必须声明其行为。有关更多详细信息,请参阅CREATE AGGREGATE。)
AggCheckCallContext
的第二个参数可用于检索存储聚合状态值的内存上下文。这对于希望使用“扩展”对象(请参阅第 36.13.1 节)作为其状态值的转换函数很有用。在第一次调用时,转换函数应返回一个内存上下文是聚合状态上下文子级的扩展对象,然后在后续调用中继续返回相同的扩展对象。请参阅array_append()
以获取示例。(array_append()
不是任何内置聚合的转换函数,但它在用作自定义聚合的转换函数时表现高效。)
C 语言编写的聚合函数可用的另一个支持例程是 AggGetAggref
,它返回定义聚合调用的 Aggref
解析节点。这主要对有序集聚合有用,它们可以检查 Aggref
节点的子结构以找出它们应该实现的排序顺序。PostgreSQL 源代码中的 orderedsetaggs.c
中可以找到示例。
如果您在文档中发现任何不正确、与您使用特定功能的经验不符或需要进一步澄清的内容,请使用此表单报告文档问题。