Redrock Postgres 搜索 英文
版本: 9.3 / 9.4 / 9.5 / 9.6 / 10 / 11 / 12 / 13 / 14 / 15 / 16 / 17

36.12. 用户自定义聚合 #

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

PostgreSQL 中的聚合函数以状态值状态转换函数定义。也就是说,聚合使用在处理每条连续输入行时更新的状态值进行操作。为了定义一个新的聚合函数,选择状态值的数据类型、状态的初始值和状态转换函数。状态转换函数获取当前行的前一状态值和聚合的输入值,并返回一个新的状态值。还可以指定一个最终函数,以防聚合的期望结果与需要保留在运行状态的值中的数据不同。最终函数获取最终状态值并返回希望作为聚合结果的内容。原则上,转换和最终函数只是普通的函数,也可以在聚合的上下文之外使用。(在实践中,出于性能原因经常有利于创建专门的转换函数,在仅在作为聚合的一部分调用时才能工作。)

因此,除了聚合用户看到的参数和结果数据类型之外,还有一个内部状态值数据类型,它可能与参数和结果类型不同。

如果我们定义一个不使用最终函数的聚合,那么我们有一个聚合计算每一行中的列值的运行函数。sum就是这种聚合的示例。sum从 0 开始,并始终将当前行的值添加到其运行总计。例如,如果我们想创建一个 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 语句,以便初始状态值为空值来实现此目的。通常,这意味着 sfunc 需要检查空状态值输入。但对于 sum 和其他一些简单的聚集(如 maxmin),将第一个非空输入值插入状态变量,然后从第二个非空输入值开始应用转换函数就足够了。PostgreSQL 在初始状态值为 null 且转换函数标记为 strict(即不会在空输入时调用)的情况下将自动执行此操作。

strict 转换函数的另一种默认行为是在遇到空输入值时,始终保持前一个状态值不变。因此,空值将被忽略。如果您需要对空输入执行其他一些行为,不要将您的转换函数声明为 strict;而是为其编写代码,以测试空输入并执行任何需要的操作。

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 编写有序集聚合,因为它们的 state 值无法定义为任何 SQL 数据类型。(在上面的示例中,注意该 state 值被声明为 type internal — 这是典型的。)另外,由于最终函数执行排序,因此不可能通过再次执行转换函数来继续添加输入行。这意味着最终函数不是READ_ONLY;必须在CREATE AGGREGATE中将其声明为READ_WRITE,或如果其他最终函数调用可以利用已经排序的状态,可以将其声明为SHAREABLE

有序集聚合的状态转换函数接收当前 state 值以及每个行的聚合输入值,并返回更新的 state 值。这与常规聚合的定义相同,但请注意未提供直接参数(如果存在)。最终函数接收最后一个 state 值、直接参数的值(如果存在),以及(如果指定了finalfunc_extra)对应于聚合输入的 null 值。与常规聚合一样,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 中找到示例。