---
source_url: https://docs.dolphindb.cn/zh/tutorials/udaf.html
fetched_at: 2026-05-19T09:48:53Z
category: tutorials
title: 自定义聚合函数
sha1: 6a67550a569f568a25c633fdc5707a2b21168142
---

# 自定义聚合函数

分布式时序数据库 DolphinDB 具有很好的扩展性。用户可以使用内置的脚本语言或者 C++ 插件来编写自定义函数（UDF）和自定义聚合函数（UDAF）。

本教程主要讲解:

- 如何使用脚本语言编写自定义聚合函数
- 聚合函数在分布式环境中的 map reduce 实现
- 聚合函数在 cgroup by 中的 running aggregate 实现。

## 1. 使用 defg 定义一个聚合函数

DolphinDB 中使用关键词 def 定义一个函数，关键词 defg 定义一个聚合函数。

下面的例子是 DolphinDB 中几何平均数的实现方法。为了防止数据的溢出，这里使用对数的方法来求几何平均。

```dolphindb
defg geometricMean(x){
    return x.log().avg().exp()
}

> geometricMean(2 4 8);
4
```

自定义聚合函数和内置聚合函数一样，除了单独作为函数调用外，也可以用于 SQL 语句。

下例为一个内存表的分组聚合计算：

```dolphindb
t = table(1 1 1 2 2 2 as id, 2.0 4.0 8.0 1.0 3.0 9.0 as value)
select geometricMean(value) as gval from t group by id
```

## 2. 聚合函数的分布式实现

对于分布式表应用自定义的聚合函数进行 SQL 语句的分组聚合计算时，情况将变得复杂。

- 当分布式表的分区字段跟 group by 的字段一致时，只需要在每个分区上分别运行该 SQL 语句，然后合并各个分区的运行结果即可。
- 当分区字段和分组字段不一致时，则需要使用 map reduce 来重新实现聚合函数。

以上文定义 geometricMean 为例，基本思路如下：

1. map：在每个分区上计算对数均值以及非空数据的个数；
2. 计算这些对数均值的加权平均，并用 exp 还原几何平均。

示例：

```dolphindb
// 普通的聚合函数声明
defg geometricMean(x){
    return x.log().avg().exp()
}

// 分布式实现
def logAvg(x) : log(x).avg()

defg geometricMeanReduce(logAvgs, counts) : wavg(logAvgs, counts).exp()

mapr geometricMean(x){logAvg(x), count(x) -> geometricMeanReduce}
```

使用 mapr 关键字将 map reduce 的实现与聚合函数关联。关键词 mapr 之后声明的是自定义聚合函数的 signature。map reduce 部分用一对花括号 `{}` 括起来。`->` 之前部分是 map 函数，之后部分是 reduce 函数。

在此例中，有两个 map 函数（logAvg 和 count），多个 map 函数之间用逗号分隔。注意，map 函数的参数必须在前面的聚合函数 signature 中定义过。

**注意**：把上面的代码粘贴到 DolphinDB 的初始化启动脚本 dolphindb.dos 中，重启 dolphindb server 后就可以在分布式 SQL 中使用这个聚合函数了。

下例用内存分区表来测试这个聚合函数。分区表用日期分区，SQL 按照股票 sym 分组，这样就能够触发聚合函数 geometricMean 的分布式实现了。

```dolphindb
t = table(2020.09.01 + 0 0 1 1 3 3 as date, take(`IBM`MSFT, 6) as sym, 1.0 2.0 3.0 4.0 9.0 8.0 as value)
db = database("", RANGE, [2020.09.01, 2020.09.03, 2020.09.10])
stock = db.createPartitionedTable(t, "stock", "date").append!(t)
select geometricMean(value) as gval from stock group by sym

sym  gval
---- ----
IBM  3
MSFT 4
```

结合此例，此处来深入分析一下 DolphinDB 的 map reduce 实现。

- map 函数作用于一个数据分区。所以若一个数据表有 n 个分区，就会产生 n 个 map 函数调用。在上面的例子中，按照日期将 stock 表分成了 2 个区，map 函数 logAvg(x) 和 count(x) 分别会被调用两次。
- reduce 函数的输入是 map 函数的结果。有几个 map 函数，reduce 的参数就有几个。每一个 reduce 的参数都是一个 vector，vector 的长度正好是分区的个数。

## 3. 聚合函数的 cgroup by 实现

cgroup by 的典型应用场景是按时间维度将数据分成几个组，然后累计计算每个组的统计值。具体来说，先计算第一个组的统计值，然后计算前两个组的所有数据的统计值，接着计算前三个组的所有数据统计值，依此类推。若严格根据此定义计算，效率很低。

一个改进的办法是分别计算每个组的一些统计值，然后根据这些统计值计算 running aggregate。这个优化算法，与 map reduce 的思路非常类似。区别是在 map reduce 的场景下，只计算一个聚合值；在 cgroup by 场景下，需要计算多个组的 running aggregate。

继续以 geometricMean 为例，演示聚合函数的 cgroup by 实现。下例增加了自定义函数 geometricMeanRunning，并且扩展了 mapr 语句：

mapr 语句的第二部分 `copy, copy->geometricMeanRunning` 是 cgroup by 的实现。map reduce 和 cgroup by 的实现用分号分隔。cgroup by 的 map 实现跟 map reduce 完全一致，所以这里直接使用了 copy 函数，也就是将 map 的结果直接传给 geometricMeanRunning。

```dolphindb
def geometricMeanRunning(logAvgs, counts) : cumwavg(logAvgs, counts).exp()

defg geometricMeanReduce(logAvgs, counts) : wavg(logAvgs, counts).exp()

mapr geometricMean(x){logAvg(x), count(x) -> geometricMeanReduce; copy, copy->geometricMeanRunning}
```

**注意**：上述变化更新到 dolphindb.dos，重启后可以在分布式 cgroup by 语句中调用聚合函数 geometricMean。

```dolphindb
t = table(2020.09.01 + 0 0 1 1 3 3 as date, take(`IBM`MSFT, 6) as sym, 1.0 2.0 3.0 4.0 9.0 8.0 as value)
select geometricMean(value) as gval from t cgroup by date order by date

date       gval
---------- -----------------
2020.09.01 1.414213562373095
2020.09.02 2.213363839400643
2020.09.04 3.464101615137754
```

## 4. 更为复杂的聚合函数例子

几何平均的实现相对简单。下面我们给出一个更为复杂的聚合函数的完整实现。这个聚合函数用于计算失衡的 t-value，基本形式定义在函数 tval 中。

```dolphindb
defg tval(x, w){
	wavgX = wavg(x, w)
	wavgX2 = wavg(x*x, w)
	return wavgX/sqrt(wavgX2 - wavgX.square())
}

def wsum2(x,w): wsum(x*x, w)

defg tvalReduce(wsumX, wsumX2, w){
	wavgX = wsumX.sum()\w.sum()
	wavgX2 = wsumX2.sum\w.sum()
	return wavgX/sqrt(wavgX2 - wavgX.square())
}

def tvalRunning(wsumX, wsumX2, w){
	wavgX = wsumX\w
	wavgX2 = wsumX2\w
	return wavgX/sqrt(wavgX2 - wavgX.square())
}

mapr tval(x, w){wsum(x, w), wsum2(x, w), contextSum(w, x)->tvalReduce; cumsum, cumsum, cumsum->tvalRunning}
```

分布式的实现，要计算三个部分：x 的加权和 wsum(x, w)，x 的平方的加权和 wsum2(x, w)，以及权重和 conextSum(w, x)。注意计算权重和时没有使用 sum(w)，而使用了 contextSum(w, x)。这是考虑到数据中可能有空值。构建这三个 map 函数后，使用 reduce 函数 tvalReduce 即可完成 tval 的分布式实现。cgroup by 的实现，需要对上面三个 map 函数的结果做一个 cumsum 的变换，然后用 tvalRunning 实现 running aggregate。
