如何编写PostgreSQL自定义聚合函数?

来源:网站建设教程作者:赵景明头衔:网络博主
导读:本期聚焦于赵景明创作的《如何编写PostgreSQL自定义聚合函数?》,敬请观看详情。想要在PostgreSQL中计算一组数据的中位数或按指定分隔符合并字符串,却发现内置聚合函数只提供SUM、AVG、COUNT这些基础能力,这时就需要自己动手创建聚合函数。PostgreSQL的自定义聚合机制允许开发者把任意状态累计逻辑封装成聚合函数,核心由状态值、状态转移函数和最终处理函数三部分组成。状态转移函数负责处理每一行输入并更新内部状态,最终函数在分组结束时把状态转换成输出结果。与普通函数相比,聚合函数还能参与GROUP BY查询和窗口计算。实现时既可以使用SQL函数,也可以使用PL/pgSQL等过程语言,甚至通过C扩展获得更高性能。本文会展示字符串聚合、中位数计算以及支持并行聚合的完整示例,并说明如何处理空值、维护聚合定义以及避免常见的类型签名错误。

PostgreSQL 提供了 SUM、AVG、COUNT、MIN、MAX 等常用聚合函数,但在业务场景中经常需要计算字符串拼接、中位数、众数或者复杂的累积统计值。内置聚合函数无法覆盖全部需求,此时可以利用 PostgreSQL 的 CREATE AGGREGATE 命令创建自定义聚合函数。自定义聚合函数与普通标量函数不同,它需要维护一个贯穿分组所有行的状态值,并通过状态转移函数不断更新这个状态,最终由可选的最终处理函数将状态转换为结果。理解这套状态机机制后,开发者即使不编写 C 扩展,也能用 SQL 或 PL/pgSQL 快速实现业务需要的聚合逻辑。

如何编写PostgreSQL自定义聚合函数?

一、自定义聚合函数的基本组成

CREATE AGGREGATE 语法中最核心的参数是 SFUNCSTYPE。其中 STYPE 定义内部状态值的类型,例如整数、数组或者复合类型;SFUNC 是状态转移函数,它接收当前状态值和当前行输入值,返回更新后的状态值。PostgreSQL 会为每个分组初始化一个状态值,然后按扫描顺序对每一行调用状态转移函数,直到分组结束。如果直接返回状态值即可作为结果,那么可以不指定 FINALFUNC;如果需要把内部状态转换成其他类型,则要通过 FINALFUNC 完成最终转换。

以一个字符串拼接聚合为例,内置的 string_agg 已经能完成大部分工作,但如果我们想忽略 NULL 值或者添加固定前缀,自定义聚合可以更灵活。下面定义一个状态类型为 text 的聚合,状态转移函数把新字符串追加到已有状态后面:

CREATE OR REPLACE FUNCTION concat_state(text, text)
RETURNS text AS
$$
SELECT COALESCE($1, '') || COALESCE($2, '')
$$ LANGUAGE sql IMMUTABLE;

CREATE AGGREGATE my_concat(text)
(
    SFUNC = concat_state,
    STYPE = text,
    INITCOND = ''
);

这里 INITCOND 用于指定初始状态值。当第一个输入行到来时,状态转移函数会以初始状态和该行值作为参数执行。注意状态转移函数必须与 STYPE 类型一致,第一个参数是状态类型,第二个参数是输入类型,返回值也必须是状态类型。上面的 COALESCE 处理了 NULL 输入,避免拼接结果变成 NULL。执行 SELECT my_concat(name) FROM users; 即可获得所有名称连接成的字符串。

如果希望最终结果与内部状态不同,可以增加 FINALFUNC。例如状态类型为数组,最终函数把数组转换成逗号分隔的文本,这样可以在最终阶段做去重或排序处理。需要特别注意的是,如果不指定 INITCOND,初始状态默认为 NULL,此时状态转移函数必须能够处理 NULL 状态值,否则会报错。

二、用PL/pgSQL实现中位数聚合

中位数是数据分析中常见的需求,但 PostgreSQL 没有内置的中位数聚合函数。我们可以使用数组作为状态类型,把分组内的所有数值累积到数组中,最后在 FINALFUNC 中排序数组并取中间值。虽然这种方式在大数据量下内存占用较高,但对于中小规模的分组非常实用,并且能清晰展示聚合函数的工作方式。

先创建一个数组追加函数,状态类型为 double precision[],状态转移函数把新元素追加到数组末尾:

CREATE OR REPLACE FUNCTION median_accum(double precision[], double precision)
RETURNS double precision[] AS
$$
SELECT array_append(COALESCE($1, ARRAY[]::double precision[]), $2)
$$ LANGUAGE sql IMMUTABLE;

CREATE OR REPLACE FUNCTION median_final(double precision[])
RETURNS double precision AS
$$
SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY x)
FROM unnest($1) AS t(x)
$$ LANGUAGE sql IMMUTABLE;

CREATE AGGREGATE median(double precision)
(
    SFUNC = median_accum,
    STYPE = double precision[],
    FINALFUNC = median_final
);

这里 median_accum 接收当前数组状态和新输入值,使用 array_append 将新值加入数组。如果没有输入值,初始状态为 NULL,COALESCE 会将其转换为空数组。最终函数 median_final 把数组展开为行集后,借助 percentile_cont 计算中位数。这样在查询中使用 SELECT department_id, median(salary) FROM employees GROUP BY department_id; 就能得到每个部门的中位数工资。

如果需要处理空值,可以在状态转移函数中过滤掉 NULL 输入,避免数组里出现 NULL 影响排序和计算。另一种做法是在最终函数中过滤 NULL,但状态函数中过滤更节省内存。此外,PL/pgSQL 函数可以编写更复杂的逻辑,比如累积前只保留固定数量的元素,或者维护两个堆来计算流式中位数,不过这些方案需要更精细的状态管理。

自定义聚合不仅能用于标量输入,还可以接收多参数输入。例如 CREATE AGGREGATE agg_name(type1, type2) 可以定义两个输入列,状态转移函数相应地接收状态、第一个输入、第二个输入。这样就能实现类似 corr 或加权平均等复杂分析。

三、并行聚合与性能优化

PostgreSQL 的并行查询会为每个工作进程独立聚合一部分数据,然后再合并各工作进程的聚合结果。要让自定义聚合函数支持并行,必须指定 COMBINEFUNC 参数。它接收两个状态值并返回合并后的状态值,相当于把两个部分结果合成一个。如果没有 COMBINEFUNC,优化器在计划并行扫描时不会考虑使用该聚合,或者只能退化为单进程模式,影响性能。

以求和聚合为例,如果我们要实现一个自定义的 my_sum,状态类型为 bigint,状态转移函数是简单加法。为了让并行聚合生效,需要定义一个合并函数 sum_combine 把两个部分和相加:

CREATE OR REPLACE FUNCTION sum_state(bigint, bigint)
RETURNS bigint AS
$$
SELECT $1 + $2
$$ LANGUAGE sql IMMUTABLE;

CREATE OR REPLACE FUNCTION sum_combine(bigint, bigint)
RETURNS bigint AS
$$
SELECT $1 + $2
$$ LANGUAGE sql IMMUTABLE;

CREATE AGGREGATE my_sum(bigint)
(
    SFUNC = sum_state,
    STYPE = bigint,
    COMBINEFUNC = sum_combine,
    PARALLEL = SAFE
);

这里 PARALLEL = SAFE 声明聚合函数可以安全地并行执行。合并函数本身也必须是不可变函数,且其参数和返回值都要与状态类型一致。PostgreSQL 不会自动推断合并逻辑,开发者必须确保状态合并的正确性。如果状态函数不满足结合律,并行聚合的结果可能与串行不一致,因此引入 COMBINEFUNC 时需要仔细验证。

对于上面使用数组累积的中位数聚合,要支持并行需要把数组连接作为合并函数。可以定义 median_combine(double precision[], double precision[]) 使用 array_cat 合并两个数组。但是需要注意,并行模式下每个工作进程可能产生部分排序数组,最终函数会在合并后的完整数组上重新排序,因此结果依然正确。状态函数和合并函数的性能直接影响聚合速度,使用 SQL 函数通常比 PL/pgSQL 函数快,但对于复杂逻辑,C 扩展是更高性能的选择。

四、维护与常见问题

PostgreSQL 的 CREATE AGGREGATE 不支持 OR REPLACE 直接替换已存在的聚合函数,修改逻辑时必须先 DROP AGGREGATE 再重新创建。例如要修改上面 my_concat 的行为,应先执行 DROP AGGREGATE my_concat(text);,然后重新执行创建语句。删除聚合时需要用参数类型指定签名,否则会报错。如果该聚合被视图或物化视图依赖,删除会失败,需要先删除或修改依赖对象。

权限方面,创建聚合需要当前用户在模式上有 CREATE 权限,并且对状态函数和最终函数有执行权限。聚合函数创建后,其他用户如果需要调用,必须被授予 EXECUTE 权限。聚合的名称不能与同模式中的其他函数或聚合冲突。虽然聚合可以重载,但不同输入参数类型的聚合需要不同的状态函数,否则容易出现模糊调用。

常见错误包括:状态转移函数的返回类型与 STYPE 不一致、初始化条件类型不匹配、合并函数缺失但设置了 PARALLEL = SAFE 导致并行查询失败、最终函数输入类型错误等。排查时可以通过 \da+ 聚合名称 查看聚合的定义,确认各函数签名。对于需要窗口函数行为的场景,可以为聚合增加 INVERSE 函数以支持移动聚合窗口,否则 PostgreSQL 会使用较慢的重新计算方式。

自定义聚合函数是 PostgreSQL 强大的扩展能力之一,合理使用能够简化 SQL 查询、封装复杂业务逻辑并保持数据库端计算的一致性。从最简单的字符串拼接,到复杂的统计分析和并行优化,掌握 CREATE AGGREGATE 的各个参数能够大幅提升数据处理的灵活度。

PostgreSQL自定义聚合函数CREATE AGGREGATE修改时间:2026-08-27 21:51:40

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。