9.3 9.4 9.5 9.6 10 11 12
阿里云PostgreSQL 问题报告 纠错本页面

35.10. 用户定义的聚集

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和一些其他 简单聚集(如maxmin), 把第一个非空输入值插入到状态变量中并且接着在第二个非空输入值上 开始应用转换函数就足够了。如果初始状态值为空并且转换函数被标记为 "strict"(即不为空输入调用), PostgreSQL会自动这样做。

"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命令。

35.10.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的普通聚集参数。

用于移动聚集模式的前向转换函数不允许返回空值作为新状态值。如果逆向转换 函数返回空值,这被当作一种指示,它表明该逆向函数无法为这个特定输入逆转 状态计算,因此该聚集计算将根据当前的帧开始位置重新从头计算。这种习惯允 许移动聚集模式被用在一些不适合逆转运行状态值的少数情况下。逆向转换函数 在这些情况下可以"撒手不管",然后在它能够工作的大部分情况中再 出来干活。例如,一个浮点数的聚集可能会在必须从运行状态值中移除一个 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的限制。

35.10.2. 多态和可变聚集

聚集函数可以使用多态状态转换函数或最终函数,这样同样的函数能被用来实现多个聚集。关于多态函数的解释可参见第 35.2.5 节。更进一步,聚集函数本身可以被指定为具有多态输入类型和状态类型,允许一个聚集函数服务于多种输入数据类型。这里是一个多态聚集的例子:

CREATE AGGREGATE array_accum (anyelement)
(
    sfunc = array_append,
    stype = anyarray,
    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 层面上没有与它等效的类型。为了表述这 种情况,可以声明最终函数为接受额外的匹配该聚集输入参数的 "dummy"参数。这种假参数总是被传递为空值,因为当最终函数被 调用时没有特定的值可用。它们的唯一用途是允许一个多态最终函数的结果类型 被连接到该聚集的输入类型。例如,内建聚集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数组,这样可以让该函数接受可变 数量的参数(见第 35.4.5 节)。 该聚集的转换函数也必须有相同的数组类型作为它们的最后一个参数。 通常这类转换函数也会被标上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是可变的,两种调用都是合法的。

出于相同的原因,在创建具有相同名称以及不同数量的常规参数的 聚集函数时一定要三思而后行。

35.10.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 — 这很典型)。

用于一个有序集聚集的状态转移函数接收当前状态值外加对于每一行的聚集输入值, 并且返回更新后的状态值。这和普通聚集的定义相同,但是注意没有提供直接参数 (如果有)。最终函数接收最后的状态值、直接参数(如果有)的值以及对应于聚 集输入的空值(如果指定了finalfunc_extra)。正如普通聚集, 只有聚集是多态时finalfunc_extra才真正有用,那时就需要额外的假 参数把最终函数的结果类型连接到该聚集的输入类型。

当前,有序集聚集不能被用做窗口函数,并且因此没有必要让它们支持移动聚集 模式。

35.10.4. 聚集的支持函数

用 C 编写的函数能够通过调用AggCheckCallContext检测它是作为聚集转换函数还是最终函数调用的,例如:

if (AggCheckCallContext(fcinfo, NULL))

检查这个区别的原因是当它为真(即一个转换函数)时,第一个输入必须是一个临时状态值并且可以因此安全地被就地修改而不是分配一个新的副本。例子可见int8inc()(这是一个函数能够安全地修改一个传引用输入的唯一情况。特别地,任何情况下用于普通聚集的最终函数不能修改它们的输入,因为在某些情况下它们将会在相同的最终状态值上被重新执行)。

另一种可用于由 C 编写的聚集函数的支持例程是 AggGetAggref,它返回定义该聚集调用的 Aggref解析节点。这主要对有序集聚集有用,它能检查 Aggref的子结构来找出它们本应实现的排序顺序。在 PostgreSQL源代码的orderedsetaggs.c 中可以找到例子。

<
/BODY >