SQL本身不支持用户直接创建聚合函数,需依赖CLR(SQL Server)或扩展机制(PostgreSQL);绝大多数场景应采用STRING_AGG、FOR XML PATH或array_to_string等现成方案模拟,仅在特殊合并逻辑(如按权重合并JSON)时才值得自定义。

SQL 本身不支持用户直接创建聚合函数(如 SUM、AVG 那类可参与 GROUP BY 的状态累积型函数),除非用 CLR(SQL Server)或扩展机制(PostgreSQL),但绝大多数场景下,你得用“模拟聚合”的方式绕过去——不是写个函数就完事,而是选对技术路径。
SQL Server 2017+:优先用 STRING_AGG,但注意排序和 NULL 处理
它不是万能的,但比手写游标或 XML PATH 稳定得多。常见错误是忽略排序依赖和空值穿透:
-
STRING_AGG(ProductName, '、')不指定WITHIN GROUP (ORDER BY ...)时,拼接顺序不确定,不同执行计划可能出不同结果 - 字段含
NULL时默认跳过,但如果你业务要求显示[空],得提前用ISNULL(ProductName, '[空]')转换 - 若拼接结果超 8000 字节(非 Unicode)或 4000 字符(Unicode),
STRING_AGG默认返回varchar(8000)或nvarchar(4000),需显式转换:CAST(STRING_AGG(...) AS NVARCHAR(MAX))
SQL Server 2016 及更早:用 FOR XML PATH('') + STUFF 组合,但防 XML 实体转义
这是旧版本最常用方案,但字段里有 &、、<code>> 会自动转成 &、<、>,导致结果失真:
- 安全写法是先用
FOR XML PATH(''), TYPE得到 XML 类型,再用.value('.', 'NVARCHAR(MAX)')提取原始字符串,绕过实体编码 - 别漏掉
STUFF(..., 1, 1, '')去掉开头多余的分隔符;如果分隔符是空格或顿号,容易误删有效字符 - 该写法在含重复值且需去重的场景下不天然支持,得套一层
DISTINCT子查询,但会显著拖慢性能
需要跨数据库兼容或逻辑复杂时:封装成标量函数,但必须警惕性能陷阱
比如写个 fn_ConcatProductNames(@OrderID),内部用游标或临时表拼接——这看起来清晰,实际是性能杀手:
- 标量函数在 SELECT 中每行调用一次,10 万订单就会执行 10 万次独立查询,I/O 和编译开销爆炸
- SQL Server 对标量 UDF 的并行计划抑制严重,即使开了并行,也可能退化为串行执行
- 真正可行的替代是内联表值函数(ITVF):返回单列单行结果集,优化器能将其展开为 JOIN,性能接近原生写法
Oracle / PostgreSQL 用户:别硬套 SQL Server 思路,它们原生支持自定义聚合
Oracle 用 CREATE OR REPLACE TYPE + ODCIAggregate 接口,PostgreSQL 用 CREATE AGGREGATE + state transition function,但这不是“写个函数就跑”,要处理:
- 初始状态(initcond)、累加逻辑(sfunc)、最终计算(finalfunc)三部分必须严格匹配类型和语义
- 并发场景下,state 值不能被多个线程污染,PostgreSQL 要求 sfunc 是 immutable,Oracle 要求 type 方法线程安全
- 调试困难:这类聚合一旦出错,错误信息往往只报“aggregate function failed”,没堆栈,得靠日志打点定位
最常被忽略的一点:所谓“自定义聚合”,90% 的真实需求其实只是字符串拼接或条件去重拼接——与其花两天写一个通用聚合类型,不如确认下目标数据库版本,选 STRING_AGG、LISTAGG 或 array_to_string(array_agg(...), ',') 这类现成方案。真正的自定义聚合,只在需要特殊合并逻辑(比如按权重合并 JSON、冲突时取最新非 NULL 值)时才值得投入。

















