Este tema describe cómo desarrollar y utilizar funciones definidas por el usuario (UDF) en Java.
Contexto
A partir de la versión 2.2.0, StarRocks admite funciones definidas por el usuario (UDF) escritas en Java.
Desde la versión 3.0, StarRocks es compatible con UDF globales. Solo necesita agregar la palabra clave GLOBAL a las instrucciones SQL correspondientes (CREATE, SHOW y DROP) para aplicarlas de forma global. Esto evita tener que ejecutar las instrucciones individualmente para cada base de datos. Puede desarrollar funciones personalizadas según sus escenarios de negocio para ampliar las capacidades de StarRocks.
Actualmente, StarRocks admite los siguientes tipos de UDF:
función definida por el usuario escalar (scalar UDF)
función de agregación definida por el usuario (UDAF)
función de ventana definida por el usuario (UDWF)
función con valor de tabla definida por el usuario (UDTF)
Requisitos previos
Antes de utilizar UDF de Java en StarRocks, asegúrese de cumplir con los siguientes requisitos:
Debe tener Apache Maven instalado para crear y desarrollar proyectos de Java.
Debe tener JDK 1.8 instalado en su servidor.
Habilite la función UDF estableciendo el elemento de configuración del FE
enable_udfenTRUEen la página Instance Configuration. A continuación, reinicie la instancia para aplicar el cambio.
Mapeo de tipos de datos
|
Tipo SQL |
Tipo Java |
|
BOOLEAN |
java.lang.Boolean |
|
TINYINT |
java.lang.Byte |
|
SMALLINT |
java.lang.Short |
|
INT |
java.lang.Integer |
|
BIGINT |
java.lang.Long |
|
FLOAT |
java.lang.Float |
|
DOUBLE |
java.lang.Double |
|
STRING/VARCHAR |
java.lang.String |
Desarrollar y utilizar UDF
Deberá crear un proyecto Maven y escribir la función en Java.
Paso 1: Crear un proyecto Maven
Cree un proyecto Maven con la siguiente estructura básica de directorio.
project
|--pom.xml
|--src
| |--main
| | |--java
| | |--resources
| |--test
|--target
Paso 2: Agregar dependencias
Agregue las siguientes dependencias al archivo pom.xml.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>udf</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>
</project>
Paso 3: Desarrollar la UDF
Desarrolle las UDF necesarias en Java.
Desarrollar una scalar UDF
Una scalar UDF procesa una fila a la vez y devuelve un único valor por cada fila. Cuando se utiliza una scalar UDF en una consulta, cada fila aparece en el conjunto de resultados. Las funciones escalares típicas incluyen UPPER, LOWER, ROUND y ABS.
El siguiente ejemplo muestra cómo extraer datos de un objeto JSON. En algunos escenarios de negocio, el valor de un campo en datos JSON puede ser una cadena JSON en lugar de un objeto JSON. Para extraer la cadena JSON anidada, debe usar una llamada anidada a la función GET_JSON_STRING, como GET_JSON_STRING(GET_JSON_STRING('{"key":"{\\"k0\\":\\"v0\\"}"}', "$.key"), "$.k0").
Para simplificar la instrucción SQL, puede desarrollar una UDF que extraiga la cadena JSON directamente; por ejemplo, MY_UDF_JSON_GET('{"key":"{\\"k0\\":\\"v0\\"}"}', "$.key.k0").
package com.starrocks.udf.sample;
import com.alibaba.fastjson.JSONPath;
public class UDFJsonGet {
public final String evaluate(String jsonObj, String key) {
if (obj == null || key == null) return null;
try {
// The JSONPath library can fully expand the path, even if a field value is a JSON-formatted string.
return JSONPath.read(jsonObj, key).toString();
} catch (Exception e) {
return null;
}
}
}
Su clase personalizada debe implementar el siguiente método.
Los tipos de datos de los parámetros de solicitud y del valor devuelto en el método deben coincidir con los declarados en la instrucción CREATE FUNCTION del paso 6. El mapeo de tipos de datos debe cumplir con las reglas descritas en Data type mapping.
|
Método |
Descripción |
|
TYPE1 evaluate(TYPE2, ...) |
El método |
Desarrollar una UDAF
Una UDAF opera sobre un grupo de filas y devuelve un único valor. Las funciones de agregación típicas incluyen SUM, COUNT, MAX y MIN. Estas funciones agregan datos de varias filas dentro de cada grupo GROUP BY y generan un único resultado por grupo.
El siguiente ejemplo utiliza la función MY_SUM_INT. A diferencia de la función integrada SUM, que devuelve un valor BIGINT, MY_SUM_INT toma un argumento INT y devuelve un INT.
package com.starrocks.udf.sample;
public class SumInt {
public static class State {
int counter = 0;
public int serializeLength() { return 4; }
}
public State create() {
return new State();
}
public void destroy(State state) {
}
public final void update(State state, Integer val) {
if (val != null) {
state.counter+= val;
}
}
public void serialize(State state, java.nio.ByteBuffer buff) {
buff.putInt(state.counter);
}
public void merge(State state, java.nio.ByteBuffer buffer) {
int val = buffer.getInt();
state.counter += val;
}
public Integer finalize(State state) {
return state.counter;
}
}
Su clase personalizada debe implementar los siguientes métodos.
Los tipos de datos de los parámetros de solicitud y del valor devuelto en los métodos deben coincidir con los declarados en la instrucción CREATE FUNCTION del paso 6. El mapeo de tipos de datos debe cumplir con las reglas descritas en Data type mapping.
|
Método |
Descripción |
|
State create() |
Crea un objeto de estado. |
|
void destroy(State) |
Destruye el objeto de estado. |
|
void update(State, ...) |
Actualiza el estado. El primer argumento es el objeto de estado y los argumentos subsiguientes son los parámetros de solicitud especificados en la declaración de la función. Se admiten uno o más parámetros de solicitud. |
|
void serialize(State, ByteBuffer) |
Serializa el estado. |
|
void merge(State, ByteBuffer) |
Fusiona un estado serializado en el estado actual. |
|
TYPE finalize(State) |
Calcula y devuelve el resultado final a partir del estado. |
Al desarrollar una UDAF, debe utilizar la clase de búfer java.nio.ByteBuffer para almacenar el resultado intermedio y el método serializeLength para especificar la longitud del resultado intermedio serializado.
|
Clase y método |
Descripción |
|
java.nio.ByteBuffer() |
Esta clase de búfer almacena el resultado intermedio. Dado que el resultado se serializa para transferirse entre nodos, debe usar |
|
serializeLength() |
La longitud en bytes del resultado intermedio serializado. El método |
Tenga en cuenta los siguientes requisitos para la serialización de java.nio.ByteBuffer:
No dependa del método
remaining()deByteBufferpara deserializar el estado.No llame al método
clear()en elByteBuffer.El valor devuelto por
serializeLength()debe coincidir con la longitud real de los datos escritos en el búfer. Una discrepancia provoca errores de serialización y deserialización.
Desarrollar una UDWF
Una UDWF es un tipo especial de función de agregación. A diferencia de una función de agregación normal, una función de ventana calcula un valor sobre un grupo de filas (una ventana) y devuelve un resultado separado para cada fila. Por lo general, una función de ventana incluye una cláusula OVER que divide las filas en particiones. Luego, la función calcula un resultado para cada fila basándose en la ventana de filas a la que pertenece.
El siguiente ejemplo utiliza la función MY_WINDOW_SUM_INT. A diferencia de la función integrada SUM, que devuelve un valor BIGINT, MY_WINDOW_SUM_INT toma un argumento INT y devuelve un INT.
package com.starrocks.udf.sample;
public class WindowSumInt {
public static class State {
int counter = 0;
public int serializeLength() { return 4; }
@Override
public String toString() {
return "State{" +
"counter=" + counter +
'}';
}
}
public State create() {
return new State();
}
public void destroy(State state) {
}
public void update(State state, Integer val) {
if (val != null) {
state.counter+=val;
}
}
public void serialize(State state, java.nio.ByteBuffer buff) {
buff.putInt(state.counter);
}
public void merge(State state, java.nio.ByteBuffer buffer) {
int val = buffer.getInt();
state.counter += val;
}
public Integer finalize(State state) {
return state.counter;
}
public void reset(State state) {
state.counter = 0;
}
public void windowUpdate(State state,
int peer_group_start, int peer_group_end,
int frame_start, int frame_end,
Integer[] inputs) {
for (int i = (int)frame_start; i < (int)frame_end; ++i) {
state.counter += inputs[i];
}
}
}
Dado que una función de ventana es un tipo especial de función de agregación, su clase personalizada debe implementar todos los métodos requeridos para UDAF, además del método windowUpdate().
Los tipos de datos de los parámetros de solicitud y del valor devuelto en los métodos deben coincidir con los declarados en la instrucción CREATE FUNCTION del paso 6. El mapeo de tipos de datos debe cumplir con las reglas descritas en Data type mapping.
Método adicional obligatorio
|
Método |
Descripción |
|
|
Actualiza los datos de la ventana. Para obtener información detallada sobre las funciones de ventana, consulte Window functions. Para cada fila de entrada, se recupera la información de ventana correspondiente para actualizar el resultado intermedio.
|
Desarrollar una UDTF
Una UDTF toma una sola fila como entrada y produce una tabla de filas de salida. Las UDTF se utilizan frecuentemente para operaciones como dividir una columna en varias filas.
Actualmente, las UDTF solo admiten la devolución de varias filas con una sola columna.
El siguiente ejemplo utiliza la función MY_UDF_SPLIT. Esta divide una cadena utilizando un espacio como separador. Tanto el argumento de entrada como el valor devuelto son del tipo STRING.
package com.starrocks.udf.sample;
public class UDFSplit{
public String[] process(String in) {
if (in == null) return null;
return in.split(" ");
}
}
Su clase personalizada debe implementar el siguiente método.
Los tipos de datos de los parámetros de solicitud y del valor devuelto en el método deben coincidir con los declarados en la instrucción CREATE FUNCTION del paso 6. El mapeo de tipos de datos debe cumplir con las reglas descritas en Data type mapping.
|
Método |
Descripción |
|
TYPE[] process() |
El método |
Paso 4: Empaquetar el proyecto Java
Ejecute el siguiente comando para empaquetar el proyecto Java.
mvn package
Este comando genera dos archivos JAR en el directorio target: udf-1.0-SNAPSHOT.jar y udf-1.0-SNAPSHOT-jar-with-dependencies.jar.
Paso 5: Cargar el proyecto
Cargue el archivo udf-1.0-SNAPSHOT-jar-with-dependencies.jar en un bucket de OSS y otorgue permiso de lectura pública al archivo JAR. Para obtener más información, consulte Simple upload y Bucket ACL.
En el paso 6, el FE valida el archivo JAR de la UDF y calcula una suma de comprobación. Posteriormente, el BE descarga y ejecuta el archivo JAR.
Paso 6: Crear la UDF en StarRocks
StarRocks proporciona dos namespaces para las UDF: nivel de base de datos y nivel global.
Si no requiere aislamiento de visibilidad para sus UDF, puede crear una UDF global. Al hacer referencia a una UDF global, puede llamarla directamente por su nombre de función sin ningún prefijo de catálogo o base de datos, lo que simplifica el acceso.
Si requiere aislamiento de visibilidad o necesita crear UDF con el mismo nombre en diferentes bases de datos, puede crear una UDF a nivel de base de datos. Si su sesión actual se encuentra dentro de esa base de datos, puede llamar a la función directamente por su nombre. Si su sesión está en un catálogo o base de datos diferente, debe usar un nombre completo, como
catalog.database.function.
La creación de una UDF global requiere el permiso CREATE GLOBAL FUNCTION a nivel de sistema. La creación de una UDF a nivel de base de datos requiere el permiso CREATE FUNCTION en la base de datos. El uso de cualquier UDF requiere el permiso USAGE sobre ella. Para obtener información sobre cómo otorgar permisos, consulte GRANT.
Después de cargar el archivo JAR, cree la UDF correspondiente en StarRocks. Para crear una UDF global, simplemente agregue la palabra clave GLOBAL a la instrucción SQL.
Sintaxis
CREATE [GLOBAL][AGGREGATE | TABLE] FUNCTION function_name(arg_type [, ...])
RETURNS return_type
[PROPERTIES ("key" = "value" [, ...]) ]
Parámetros
|
Parámetro |
Obligatorio |
Descripción |
|
GLOBAL |
No |
Especifica que la UDF es una UDF global. Compatible desde StarRocks v3.0. |
|
AGGREGATE |
No |
Especifica que la función que se va a crear es una UDAF o UDWF. |
|
TABLE |
No |
Especifica que la función que se va a crear es una UDTF. |
|
function_name |
Sí |
El nombre de la función. Puede incluir un nombre de base de datos, como |
|
arg_type |
Sí |
El tipo de datos de un argumento de función. Para ver los tipos de datos compatibles, consulte Data type mapping. |
|
return_type |
Sí |
El tipo de datos del valor devuelto por la función. Para ver los tipos de datos compatibles, consulte Data type mapping. |
|
properties |
Sí |
Propiedades relacionadas con la función. Se requieren diferentes propiedades para distintos tipos de UDF. Consulte los siguientes ejemplos para obtener más detalles. |
Crear una scalar UDF
Ejecute el siguiente comando para crear la scalar UDF del ejemplo anterior en StarRocks.
CREATE [GLOBAL] FUNCTION MY_UDF_JSON_GET(string, string)
RETURNS string
PROPERTIES (
"symbol" = "com.starrocks.udf.sample.UDFJsonGet",
"type" = "StarrocksJar",
"file" = "http://<YourBucketName>.oss-cn-xxxx-internal.aliyuncs.com/<YourPath>/udf-1.0-SNAPSHOT-jar-with-dependencies.jar"
);
|
Parámetro |
Descripción |
|
symbol |
El nombre de clase completo de la UDF, en el formato |
|
type |
El tipo de UDF. Para UDF de Java, establézcalo en |
|
file |
La ruta HTTP al archivo JAR de la UDF. Debe ser la URL HTTP del archivo en OSS, preferiblemente utilizando un endpoint interno. El formato es |
Crear una UDAF
Ejecute el siguiente comando para crear la UDAF del ejemplo anterior en StarRocks.
CREATE [GLOBAL] AGGREGATE FUNCTION MY_SUM_INT(INT)
RETURNS INT
PROPERTIES
(
"symbol" = "com.starrocks.udf.sample.SumInt",
"type" = "StarrocksJar",
"file" = "http://<YourBucketName>.oss-cn-xxxx-internal.aliyuncs.com/<YourPath>/udf-1.0-SNAPSHOT-jar-with-dependencies.jar"
);
Los parámetros en la cláusula PROPERTIES son los mismos que se describen en Create a scalar UDF.
Crear una UDWF
Ejecute el siguiente comando para crear la UDWF del ejemplo anterior en StarRocks.
CREATE [GLOBAL] AGGREGATE FUNCTION MY_WINDOW_SUM_INT(Int)
RETURNS Int
PROPERTIES
(
"analytic" = "true",
"symbol" = "com.starrocks.udf.sample.WindowSumInt",
"type" = "StarrocksJar",
"file" = "http://<YourBucketName>.oss-cn-xxxx-internal.aliyuncs.com/<YourPath>/udf-1.0-SNAPSHOT-jar-with-dependencies.jar"
);
La propiedad analytic identifica la función como una función de ventana. Para una UDWF, establézcala en true. Los demás parámetros son los mismos que se describen en Create a scalar UDF.
Crear una UDTF
Ejecute el siguiente comando para crear la UDTF del ejemplo anterior en StarRocks.
CREATE [GLOBAL] TABLE FUNCTION MY_UDF_SPLIT(string)
RETURNS string
PROPERTIES
(
"symbol" = "com.starrocks.udf.sample.UDFSplit",
"type" = "StarrocksJar",
"file" = "http://<YourBucketName>.oss-cn-xxxx-internal.aliyuncs.com/<YourPath>/udf-1.0-SNAPSHOT-jar-with-dependencies.jar"
);
Los parámetros en la cláusula PROPERTIES son los mismos que se describen en Create a scalar UDF.
Paso 7: Utilizar la UDF
Después de crear la UDF, puede probarla y utilizarla.
Utilizar la scalar UDF
Ejecute el siguiente comando para utilizar la scalar UDF creada en el paso 6.
SELECT MY_UDF_JSON_GET('{"key":"{\\"in\\":2}"}', '$.key.in');
Utilizar la UDAF
Ejecute el siguiente comando para utilizar la UDAF creada en el paso 6.
SELECT MY_SUM_INT(col1);
Utilizar la UDWF
Ejecute el siguiente comando para utilizar la UDWF creada en el paso 6.
SELECT MY_WINDOW_SUM_INT(intcol)
OVER (PARTITION BY intcol2
ORDER BY intcol3
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
FROM test_basic;
Utilizar la UDTF
Ejecute el siguiente comando para utilizar la UDTF del ejemplo anterior.
-- Assume that table t1 exists with columns a, b, and c1.
SELECT t1.a,t1.b,t1.c1 FROM t1;
> output:
1,2.1,"hello world"
2,2.2,"hello UDTF."
-- Use the MY_UDF_SPLIT() function.
SELECT t1.a,t1.b, MY_UDF_SPLIT FROM t1, MY_UDF_SPLIT(t1.c1);
> output:
1,2.1,"hello"
1,2.1,"world"
2,2.2,"hello"
2,2.2,"UDTF."
En la lista SELECT, el primer
MY_UDF_SPLITes el alias de columna predeterminado para la salida de la funciónMY_UDF_SPLIT.Actualmente, no puede especificar un alias de tabla ni alias de columna para la salida de la UDTF, como con
AS t2(f1).
Consultar UDF
Ejecute el siguiente comando para consultar información sobre las UDF.
SHOW [GLOBAL] FUNCTIONS;
Eliminar UDF
Ejecute el siguiente comando para eliminar una UDF específica.
DROP [GLOBAL] FUNCTION <function_name>(arg_type [, ...]);
Preguntas frecuentes
P: ¿Puedo usar variables estáticas al desarrollar una UDF? ¿Interferirán entre sí las variables estáticas de diferentes UDF?
R: Sí, puede usar variables estáticas. Las variables estáticas están aisladas entre diferentes UDF y no interfieren entre sí, incluso si las UDF comparten el mismo nombre de clase.