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之外的其他聚集函数)。

聚集函数可以使用多态状态转换函数或最终函数,这样同样的函数能被用来实现多个聚集。关于多态函数的解释可参见Section 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)

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

if (AggCheckCallContext(fcinfo, NULL))

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

更多细节可以参考CREATE AGGREGATE命令。