用户定义的聚合函数
如需在预览版期间获得支持,请发送电子邮件至 bigquery-sql-preview-support@google.com。
本文档介绍如何在 BigQuery 中创建、调用和删除用户定义的聚合函数 (UDAF)。
借助 UDAF,您可以使用包含代码的表达式创建聚合函数。UDAF 接受输入列,一次对一组行执行计算,然后将该计算的结果作为单个值返回。
创建 SQL UDAF
本部分介绍在 BigQuery 中创建永久性或临时 SQL UDAF 的各种方法。
创建永久性 SQL UDAF
您可以创建一个永久性 SQL UDAF,这意味着您可以在多个查询中重复使用该 UDAF。在所有者之间共享永久性 UDAF 时,可以安全调用这些 UDAF。UDAF 无法变更数据、与外部系统通信,也无法将日志发送到 Google Cloud Observability 或类似应用。
如需创建永久性 UDAF,请使用 CREATE AGGREGATE FUNCTION
语句,并且不带 TEMP
或 TEMPORARY
关键字。您必须在函数路径中包含数据集。
例如,以下查询会创建一个名为 ScaledAverage
的永久性 UDAF:
CREATE AGGREGATE FUNCTION myproject.mydataset.ScaledAverage( dividend FLOAT64, divisor FLOAT64) RETURNS FLOAT64 AS ( AVG(dividend / divisor) );
创建临时 SQL UDAF
您可以创建临时的 SQL UDAF,这意味着 UDAF 仅存在于一个查询、脚本、会话或过程的范围内。
如需创建临时 UDAF,请使用 CREATE AGGREGATE FUNCTION
语句和 TEMP
或 TEMPORARY
关键字。
例如,以下查询会创建一个名为 ScaledAverage
的临时 UDAF:
CREATE TEMP AGGREGATE FUNCTION ScaledAverage( dividend FLOAT64, divisor FLOAT64) RETURNS FLOAT64 AS ( AVG(dividend / divisor) );
使用聚合和非聚合参数
您可以创建一个同时包含聚合参数和非聚合参数的 SQL UDAF。
UDAF 通常会聚合组中所有行的函数参数。但是,您可以使用 NOT AGGREGATE
关键字将函数参数指定为非聚合。
非聚合函数参数是一个标量函数参数,一个组中的所有行都具有常量值。有效的非聚合函数参数必须是字面量。在 UDAF 定义中,聚合函数参数只能作为聚合函数调用的函数参数。对非聚合函数参数的引用可以出现在 UDAF 定义中的任何位置。
例如,以下函数包含一个名为 dividend
的聚合参数和一个名为 divisor
的非聚合参数:
-- Create the function. CREATE TEMP AGGREGATE FUNCTION ScaledSum( dividend FLOAT64, divisor FLOAT64 NOT AGGREGATE) RETURNS FLOAT64 AS ( SUM(dividend) / divisor );
在函数正文中使用默认项目
在 SQL UDAF 的主体中,对 BigQuery 实体(例如表或视图)的任何引用都必须包含项目 ID,除非该实体位于包含 UDAF 的项目中。
例如,请参考以下语句:
CREATE AGGREGATE FUNCTION project1.dataset_a.ScaledAverage( dividend FLOAT64, divisor FLOAT64) RETURNS FLOAT64 AS ( ( SELECT AVG(dividend / divisor) FROM dataset_a.my_table ) );
如果您在 project1
项目中运行上述语句,则语句会成功,因为 project1
中存在 my_table
。但是,如果您从其他项目运行上述语句,该语句将失败。如需更正此错误,请在表引用中添加项目 ID:
CREATE AGGREGATE FUNCTION project1.dataset_a.ScaledAverage( dividend FLOAT64, divisor FLOAT64) RETURNS FLOAT64 AS ( ( SELECT AVG(dividend / divisor) FROM project1.dataset_a.my_table ) );
您还可以引用位于不同于在其中创建函数的项目或数据集的其他项目或数据集中的实体:
CREATE AGGREGATE FUNCTION project1.dataset_a.ScaledAverage( dividend FLOAT64, divisor FLOAT64) RETURNS FLOAT64 AS ( ( SELECT AVG(dividend / divisor) FROM project2.dataset_c.my_table ) );
创建 JavaScript UDAF
本部分介绍在 BigQuery 中创建 JavaScript UDAF 的各种方法。创建 JavaScript UDAF 时,需要遵守以下几项规则:
JavaScript UDAF 的正文必须是带英文引号的字符串字面量,用于表示 JavaScript 代码。如需详细了解您可以使用的不同类型的带英文引号的字符串字面量,请参阅带英文引号的字面量的格式。
仅允许使用特定类型编码。如需了解详情,请参阅 JavaScript UDAF 中允许的 SQL 类型编码。
JavaScript 函数正文必须包含四个 JavaScript 函数,用于初始化、聚合、合并和最终确定 JavaScript UDAF 的结果(
initialState
、aggregate
、merge
和finalize
)。如需了解详情,请参阅 JavaScript UDAF 中允许的 SQL 类型编码。由
initialState
函数返回的任何值或是调用aggregate
或merge
函数后留在state
参数中的任何值都必须可序列化。如果要处理不可序列化的聚合数据(例如函数或符号字段),则必须使用所包含的serialize
和deserialize
函数。如需了解详情,请参阅在 JavaScript UDAF 中序列化和反序列化数据。
创建永久性 JavaScript UDAF
您可以创建一个永久性 JavaScript UDAF,这意味着您可以在多个查询中重复使用该 UDAF。在所有者之间共享永久性 UDAF 时,可以安全调用这些 UDAF。UDAF 无法变更数据、与外部系统通信,也无法将日志发送到 Google Cloud Observability 或类似应用。
如需创建永久性 UDAF,请使用 CREATE AGGREGATE FUNCTION
语句,并且不带 TEMP
或 TEMPORARY
关键字。您必须在函数路径中包含数据集。
以下查询会创建一个名为 SumPositive
的永久性 JavaScript UDAF:
CREATE OR REPLACE AGGREGATE FUNCTION my_project.my_dataset.SumPositive(x FLOAT64) RETURNS FLOAT64 LANGUAGE js AS r''' export function initialState() { return {sum: 0} } export function aggregate(state, x) { if (x > 0) { state.sum += x; } } export function merge(state, partialState) { state.sum += partialState.sum; } export function finalize(state) { return state.sum; } '''; -- Call the JavaScript UDAF. WITH numbers AS ( SELECT * FROM UNNEST([1.0, -1.0, 3.0, -3.0, 5.0, -5.0]) AS x) SELECT my_project.my_dataset.SumPositive(x) AS sum FROM numbers; /*-----* | sum | +-----+ | 9.0 | *-----*/
创建临时 JavaScript UDAF
您可以创建临时 JavaScript UDAF,这意味着 UDAF 仅存在于单个查询、脚本、会话或过程的范围内。
如需创建临时 UDAF,请使用 CREATE AGGREGATE FUNCTION
语句和 TEMP
或 TEMPORARY
关键字。
以下查询会创建一个名为 SumPositive
的临时 JavaScript UDAF:
CREATE TEMP AGGREGATE FUNCTION SumPositive(x FLOAT64) RETURNS FLOAT64 LANGUAGE js AS r''' export function initialState() { return {sum: 0} } export function aggregate(state, x) { if (x > 0) { state.sum += x; } } export function merge(state, partialState) { state.sum += partialState.sum; } export function finalize(state) { return state.sum; } '''; -- Call the JavaScript UDAF. WITH numbers AS ( SELECT * FROM UNNEST([1.0, -1.0, 3.0, -3.0, 5.0, -5.0]) AS x) SELECT SumPositive(x) AS sum FROM numbers; /*-----* | sum | +-----+ | 9.0 | *-----*/
在 JavaScript UDAF 中包含非聚合参数
您可以创建一个同时包含聚合参数和非聚合参数的 JavaScript UDAF。
UDAF 通常会聚合组中所有行的函数参数。但是,您可以使用 NOT AGGREGATE
关键字将函数参数指定为非聚合。
非聚合函数参数是一个标量函数参数,一个组中的所有行都具有常量值。有效的非聚合函数参数必须是字面量。在 UDAF 定义中,聚合函数参数只能作为聚合函数调用的函数参数。对非聚合函数参数的引用可以出现在 UDAF 定义中的任何位置。
在以下示例中,JavaScript UDAF 包含一个名为 s
的聚合参数和一个名为 delimiter
的非聚合参数:
CREATE TEMP AGGREGATE FUNCTION JsStringAgg( s STRING, delimiter STRING NOT AGGREGATE) RETURNS STRING LANGUAGE js AS r''' export function initialState() { return {strings: []} } export function aggregate(state, s) { state.strings.push(s); } export function merge(state, partialState) { state.strings = state.strings.concat(partialState.strings); } export function finalize(state, delimiter) { return state.strings.join(delimiter); } '''; -- Call the JavaScript UDAF. WITH strings AS ( SELECT * FROM UNNEST(["aaa", "bbb", "ccc", "ddd"]) AS values) SELECT JsStringAgg(values, '.') AS result FROM strings; /*-----------------* | result | +-----------------+ | aaa.bbb.ccc.ddd | *-----------------*/
在 JavaScript UDAF 中序列化和反序列化数据
BigQuery 必须序列化 initialState
函数返回的任何对象,或是调用 aggregate
或 merge
函数后留在 state
参数中的任何对象。如果所有字段都是以下各项之一,则 BigQuery 支持序列化对象:
- JavaScript 原初值(例如:
2
、"abc"
、null
、undefined
)。 - BigQuery 支持为其序列化所有字段值的 JavaScript 对象。
- BigQuery 支持为其序列化元素的 JavaScript 数组。
以下返回值可序列化:
export function initialState() {
return {a: "", b: 3, c: null, d: {x: 23} }
}
export function initialState() {
return {value: 2.3};
}
以下返回值不可序列化:
export function initialState() {
return {
value: function() {return 6;}
}
}
export function initialState() {
return 2.3;
}
如果您要处理不可序列化的聚合状态,则 JavaScript UDAF 必须包含 serialize
和 deserialize
函数。serialize
函数会将聚合状态转换为可序列化对象;deserialize
函数会将可序列化对象转换回聚合状态。
在以下示例中,外部库使用接口计算总和:
export class SumAggregator { constructor() { this.sum = 0; } update(value) { this.sum += value; } getSum() { return this.sum; } }
以下查询不会执行,因为 SumAggregator
类对象不可由 BigQuery 序列化,原因是该类内部存在函数。
CREATE TEMP AGGREGATE FUNCTION F(x FLOAT64) RETURNS FLOAT64 LANGUAGE js AS r''' class SumAggregator { constructor() { this.sum = 0; } update(value) { this.sum += value; } getSum() { return this.sum; } } export function initialState() { return new SumAggregator(); } export function aggregate(agg, value) { agg.update(value); } export function merge(agg1, agg2) { agg1.update(agg2.getSum()); } export function finalize(agg) { return agg.getSum(); } '''; --Error: getSum is not a function SELECT F(x) AS results FROM UNNEST([1,2,3,4]) AS x;
如果向上述查询添加 serialize
和 deserialize
函数,则查询会运行,因为 SumAggregator
类对象会转换为可由 BigQuery 序列化的对象,然后再次转换回 SumAggregator
类对象。
CREATE TEMP AGGREGATE FUNCTION F(x FLOAT64) RETURNS FLOAT64 LANGUAGE js AS r''' class SumAggregator { constructor() { this.sum = 0; } update(value) { this.sum += value; } getSum() { return this.sum; } } export function initialState() { return new SumAggregator(); } export function aggregate(agg, value) { agg.update(value); } export function merge(agg1, agg2) { agg1.update(agg2.getSum()); } export function finalize(agg) { return agg.getSum(); } export function serialize(agg) { return {sum: agg.getSum()}; } export function deserialize(serialized) { var agg = new SumAggregator(); agg.update(serialized.sum); return agg; } '''; SELECT F(x) AS results FROM UNNEST([1,2,3,4]) AS x; /*-----------------* | results | +-----------------+ | 10.0 | *-----------------*/
如需详细了解序列化函数,请参阅可选的 JavaScript 序列化函数。
在 JavaScript UDAF 中包含全局变量和自定义函数
JavaScript 函数正文可以包含自定义 JavaScript 代码,例如 JavaScript 全局变量和自定义函数。
全局变量在 JavaScript 加载到 BigQuery 时且在 initialState
函数执行之前执行。如果您需要执行不应对每个聚合组重复执行的一次性初始化工作(initialState
、aggregate
、merge
和 finalize
函数便是这种情况),全局变量可能会十分有用。
不要使用全局变量存储聚合状态。而是将聚合状态限制为传递给导出函数的对象。仅使用全局变量缓存不特定于任何特定聚合操作的开销较大的操作。
在以下查询中,SumOfPrimes
函数计算总和,但计算中仅包含素数。JavaScript 函数正文中有两个全局变量 primes
和 maxTested
,它们首先进行初始化。此外,有一个名为 isPrime
的自定义函数,用于检查数字是否为素数。
CREATE TEMP AGGREGATE FUNCTION SumOfPrimes(x INT64) RETURNS INT64 LANGUAGE js AS r''' var primes = new Set([2]); var maxTested = 2; function isPrime(n) { if (primes.has(n)) { return true; } if (n <= maxTested) { return false; } for (var k = 2; k < n; ++k) { if (!isPrime(k)) { continue; } if ((n % k) == 0) { maxTested = n; return false; } } maxTested = n; primes.add(n); return true; } export function initialState() { return {sum: 0}; } export function aggregate(state, x) { x = Number(x); if (isPrime(x)) { state.sum += x; } } export function merge(state, partialState) { state.sum += partialState.sum; } export function finalize(state) { return state.sum; } '''; -- Call the JavaScript UDAF. WITH numbers AS ( SELECT * FROM UNNEST([10, 11, 13, 17, 19, 20]) AS x) SELECT SumOfPrimes(x) AS sum FROM numbers; /*-----* | sum | +-----+ | 60 | *-----*/
包括 JavaScript 库
您可以在 OPTIONS
子句中使用 library
选项扩展 JavaScript UDAF。通过此选项,您可以为 JavaScript UDAF 指定外部代码库,然后使用 import
声明导入这些库。
在以下示例中,bar.js
中的代码可用于 JavaScript UDAF 函数正文中的任何代码:
CREATE TEMP AGGREGATE FUNCTION JsAggFn(x FLOAT64) RETURNS FLOAT64 LANGUAGE js OPTIONS (library = ['gs://foo/bar.js']) AS r''' import doInterestingStuff from 'bar.js'; export function initialState() { return ... } export function aggregate(state, x) { var result = doInterestingStuff(x); ... } export function merge(state, partial_state) { ... } export function finalize(state) { return ...; } ''';
必需的 JavaScript 结构
与 JavaScript UDF(其中的函数正文是针对每行运行的自由形式 JavaScript)不同,JavaScript UDAF 的函数正文是一个 JavaScript 模块,其中包含一些在聚合过程的各个阶段进行调用的内置导出函数。其中一些内置函数是必需的,而其他函数是可选的。您还可以添加自己的 JavaScript 函数。
必需的 JavaScript 聚合函数
您可以包含自己的 JavaScript 函数,但 JavaScript 函数正文必须包含以下可导出的 JavaScript 函数:
initialState([nonAggregateParam])
:返回一个 JavaScript 对象,表示尚未聚合任何行的聚合状态。aggregate(state, aggregateParam[, ...][, nonAggregateParam])
:聚合一行数据,更新状态以存储聚合结果。不返回值。merge(state, partialState, [nonAggregateParam])
:将聚合状态partialState
合并为聚合状态state
。当引擎并行聚合数据的不同部分并且需要组合结果时,会使用此函数。不返回值。finalize(finalState, [nonAggregateParam])
:在给定最终聚合状态finalState
的情况下,返回聚合函数的最终结果。
如需详细了解必需函数,请参阅 JavaScript UDAF 中的必需函数。
可选的 JavaScript 序列化函数
如果您要处理不可序列化的聚合状态,则 JavaScript UDAF 必须提供 serialize
和 deserialize
函数。serialize
函数会将聚合状态转换为可由 BigQuery 序列化的对象;deserialize
函数会将可由 BigQuery 序列化的对象转换回聚合状态。
serialize(state)
:返回一个要通过deserialize
函数进行反序列化的可序列化对象(其中包含聚合状态中的信息)。deserialize(serializedState)
:将serializedState
(之前由serialize
函数进行序列化)反序列化为可以传递到serialize
、aggregate
、merge
或finalize
函数中的聚合状态。
如需详细了解内置 JavaScript 序列化函数,请参阅 JavaScript UDAF 的序列化函数。
如需了解如何使用 JavaScript UDAF 序列化和反序列化数据,请参阅在 JavaScript UDAF 中序列化和反序列化数据。
JavaScript UDAF 中允许的 SQL 类型编码
在 JavaScript UDAF 中,以下受支持的 GoogleSQL 数据类型表示 JavaScript 数据类型,如下所示:
GoogleSQL 数据类型 |
JavaScript 数据类型 |
备注 |
---|---|---|
ARRAY |
Array |
不支持数组的数组。如需绕过此限制,请使用 Array<Object<Array>> (JavaScript) 和 ARRAY<STRUCT<ARRAY>> (GoogleSQL) 数据类型。
|
BIGNUMERIC
|
Number 或 String
|
与 NUMERIC 相同。
|
BOOL |
Boolean |
|
BYTES |
Uint8Array |
|
DATE |
Date |
|
FLOAT64 |
Number |
|
INT64 |
BigInt |
|
JSON |
各种类型 |
GoogleSQL JSON 数据类型可以转换为 JavaScript Object 、Array 或 GoogleSQL 支持的其他 JavaScript 数据类型。
|
NUMERIC
|
Number 或 String
|
如果 NUMERIC 值可精确表示为 IEEE 754 浮点值(范围为 [-253, 253] ),且没有小数部分,则系统会将此值编码为 Number 数据类型,否则编码为 String 数据类型。
|
STRING |
String |
|
STRUCT |
Object |
每个 STRUCT 字段都是 Object 数据类型中的命名属性。不支持未命名 STRUCT 字段。
|
TIMESTAMP |
Date |
Date 包含一个微秒字段,其中是 TIMESTAMP 的微秒部分。
|
调用 UDAF
本部分介绍在 BigQuery 中创建永久性或临时 UDAF 后,您可以对其进行调用的各种方法。
调用永久性 UDAF
您可以采取与调用内置聚合函数相同的方式调用永久性 UDAF。如需了解详情,请参阅聚合函数调用。您必须在函数路径中包含数据集。
在以下示例中,查询会调用名为 WeightedAverage
的永久性 UDAF:
SELECT my_project.my_dataset.WeightedAverage(item, weight, 2) AS weighted_average FROM ( SELECT 1 AS item, 2.45 AS weight UNION ALL SELECT 3 AS item, 0.11 AS weight UNION ALL SELECT 5 AS item, 7.02 AS weight );
系统会生成包含以下结果的表:
/*------------------*
| weighted_average |
+------------------+
| 4.5 |
*------------------*/
调用临时 UDAF
您可以采取与调用内置聚合函数相同的方式调用临时 UDAF。如需了解详情,请参阅聚合函数调用。
临时函数必须包含在包含 UDAF 函数调用的多语句查询或过程中。
在以下示例中,查询会调用名为 WeightedAverage
的临时 UDAF:
CREATE TEMP AGGREGATE FUNCTION WeightedAverage(...) -- Temporary UDAF function call SELECT WeightedAverage(item, weight, 2) AS weighted_average FROM ( SELECT 1 AS item, 2.45 AS weight UNION ALL SELECT 3 AS item, 0.11 AS weight UNION ALL SELECT 5 AS item, 7.02 AS weight );
系统会生成包含以下结果的表:
/*------------------*
| weighted_average |
+------------------+
| 4.5 |
*------------------*/
忽略或包含具有 NULL
值的行
使用 IGNORE NULLS
参数调用 JavaScript UDAF 时,BigQuery 会自动跳过其任何聚合参数的求值结果为 NULL
的行。此类行会完全从聚合中排除,不会传递给 JavaScript aggregate
函数。如果提供了 RESPECT NULLS
参数,系统会停用 NULL
过滤,无论 NULL
值如何,每一行都会传递给 JavaScript UDAF。
如果 IGNORE NULLS
和 RESPECT NULLS
参数均未提供,则默认参数为 IGNORE NULLS
。
以下示例展示了默认 NULL
行为、IGNORE NULLS
行为和 RESPECT NULLS
行为:
CREATE TEMP AGGREGATE FUNCTION SumPositive(x FLOAT64) RETURNS FLOAT64 LANGUAGE js AS r''' export function initialState() { return {sum: 0} } export function aggregate(state, x) { if (x == null) { // Use 1000 instead of 0 as placeholder for null so // that NULL values passed are visible in the result. state.sum += 1000; return; } if (x > 0) { state.sum += x; } } export function merge(state, partialState) { state.sum += partialState.sum; } export function finalize(state) { return state.sum; } '''; -- Call the JavaScript UDAF. WITH numbers AS ( SELECT * FROM UNNEST([1.0, 2.0, NULL]) AS x) SELECT SumPositive(x) AS sum, SumPositive(x IGNORE NULLS) AS sum_ignore_nulls, SumPositive(x RESPECT NULLS) AS sum_respect_nulls FROM numbers; /*-----+------------------+-------------------* | sum | sum_ignore_nulls | sum_respect_nulls | +-----+------------------+-------------------+ | 3.0 | 3.0 | 1003.0 | *-----+------------------+-------------------*/
删除 UDAF
本部分介绍在 BigQuery 中创建永久性或临时 UDAF 后将其删除的各种方法。
删除永久性 UDAF
如需删除永久性 UDAF,请使用 DROP FUNCTION
语句。您必须在函数路径中包含数据集。
在以下示例中,查询会删除名为 WeightedAverage
的永久性 UDAF:
DROP FUNCTION IF EXISTS my_project.my_dataset.WeightedAverage;
删除临时 UDAF
如需删除临时 UDAF,请使用 DROP FUNCTION
语句。
在以下示例中,查询会删除名为 WeightedAverage
的临时 UDAF:
DROP FUNCTION IF EXISTS WeightedAverage;
临时 UDAF 会在查询完成后立即失效。UDAF 无需删除,除非您想尽早从多语句查询或过程中将其移除。
列出 UDAF
UDAF 是一种例程。如需列出数据集中的所有例程,请参阅列出例程。
性能提示
如果要提高查询的性能,请考虑以下事项:
对输入进行预先过滤。在 JavaScript 中处理数据比在 SQL 中开销更大,因此最好先在 SQL 中尽可能多地过滤输入。
以下查询效率较低,因为它在 UDAF 调用中使用
x > 0
过滤输入:SELECT JsFunc(x) FROM t;
以下查询效率较高,因为它在调用 UDAF 之前使用
WHERE x > 0
对输入进行预先过滤:SELECT JsFunc(x) FROM t WHERE x > 0;
尽可能使用内置聚合函数,而非 JavaScript。在 JavaScript 中重新实现内置聚合函数的速度比调用执行相同操作的内置聚合函数更慢。
以下查询效率较低,因为它是实现 UDAF:
SELECT SumSquare(x) FROM t;
以下查询效率较高,因为它是实现可生成与上一个查询相同结果的内置函数:
SELECT SUM(x*x) FROM t;
JavaScript UDAF 适用于更复杂的聚合操作,这类操作无法通过内置函数来表示。
高效使用内存。在 JavaScript 处理环境中,每个查询可使用的内存有限。累积太多本地状态的 JavaScript UDAF 查询可能会因内存耗尽而失败。请特别注意尽可能减小聚合状态对象的大小,并避免累积大量行的聚合状态。
以下查询效率低下,因为当处理的行数变多时,
aggregate
函数会使用无限量的内存。export function initialState() { return {rows: []}; } export function aggregate(state, x) { state.rows.push(x); } ...
尽可能使用分区表。对分区表进行查询时,JavaScript UDAF 的运行效率通常会高于对非分区表进行查询时的效率,因为与非分区表相比,分区表会将数据存储在许多较小的文件中,因而能够实现较高的并行处理。
限制
UDAF 具有与 UDF 相同的限制。如需了解详情,请参阅 UDF 限制。
只有字面量、查询参数和脚本变量才能作为 UDAF 的非聚合参数传入。
不支持在 JavaScript UDAF 函数调用中使用
ORDER BY
子句。SELECT MyUdaf(x ORDER BY y) FROM t; -- Error: ORDER BY is unsupported.
价格
UDAF 采用标准 BigQuery 价格模式计费。
配额和限制
UDAF 具有与 UDF 相同的配额和限制。如需了解 UDF 配额,请参阅配额和限制。