Flink de la serie de entrada a la competencia (9)

11.7, función

Las funciones en Flink SQL se pueden dividir en dos categorías: una es la función del sistema integrada en SQL, que se puede llamar directamente a través del nombre de la función y puede realizar algunas operaciones de conversión comunes, como COUNT() y CHAR_LENGTH() usamos antes, UPPER(), etc., y otro tipo de función es una función definida por el usuario (UDF), que debe registrarse en el entorno de la tabla antes de que pueda usarse.

11.7.1 Funciones del sistema

Las funciones del sistema, también llamadas funciones integradas, son módulos funcionales preimplementados en el sistema. Podemos llamar directamente a través del nombre de la función fija para lograr la operación de conversión deseada.

Flink SQL proporciona una gran cantidad de funciones del sistema y admite casi todas las operaciones en SQL estándar, lo que nos brinda una gran comodidad para usar SQL para escribir programas de procesamiento de flujo. Las funciones del sistema en Flink SQL se pueden dividir principalmente en dos categorías: funciones escalares (Scalar Functions) y funciones agregadas (Agregate Functions).

11.7.1.1 Funciones escalares

El llamado "escalar" se refiere a una cantidad que tiene solo un valor numérico y no tiene dirección; por lo tanto, una función escalar se refiere a una función que solo realiza operaciones de conversión en datos de entrada y devuelve un valor. Los datos de entrada aquí corresponden a la tabla, que generalmente es uno o más campos en una fila de datos, por lo que esta operación es un poco como el mapa en el operador de conversión de procesamiento de flujo. Además, algunas funciones que no tienen parámetros de entrada y pueden obtener directamente un resultado único también son funciones escalares.

Las funciones escalares son el tipo de funciones de sistema más común y simple, y el número es muy grande, muchas de las cuales también están definidas en SQL estándar. Así que aquí solo enumeramos algunas funciones para algunos tipos comunes y hacemos una breve descripción general. Para aplicaciones específicas, puede ver la lista completa de funciones en el sitio web oficial.

  • Funciones de comparación (Funciones de comparación)
    La función de comparación es en realidad una expresión de comparación, que se utiliza para juzgar la relación entre dos valores y devolver un valor booleano. Esta expresión de comparación se puede usar para conectar dos valores con símbolos como <, >, =, o puede ser un juicio determinado definido por palabras clave. Por ejemplo:
    • valor1 = valor2 juzga que dos valores son iguales;
    • valor1 <> valor2 juzga que dos valores no son iguales
    • el valor NO ES NULO Determina que el valor no está vacío
  • Funciones lógicas (Funciones lógicas)
    Las funciones lógicas son una expresión lógica, es decir, use AND (AND) o (OR), not (NOT) para conectar valores booleanos, y también se pueden usar declaraciones de juicio (IS, IS NOT) Hacer un juicio de verdad; devolver un valor booleano. Por ejemplo:
    • boolean1 OR boolean2 Valor booleano boolean1 y valor booleano boolean2 toman OR lógico
    • booleano ES FALSO Determinar si el valor booleano es falso
    • NO booleano Booleano booleano toma NOT lógico
  • Funciones aritméticas¶

Funciones que realizan cálculos aritméticos, incluidas operaciones vinculadas con símbolos aritméticos y operaciones matemáticas complejas. Por ejemplo:

  • numeric1 + numeric2 suma dos números
  • POWER(numeric1, numeric2) exponenciación, lleve el número numeric1 a la potencia de numeric2
  • RAND() devuelve un número pseudoaleatorio de tipo double en el intervalo (0.0, 1.0)
  • Funciones de cadena¶

Funciones para la manipulación de cadenas. Por ejemplo:

  • string1 || string2 concatenación de dos cadenas
  • SUPERIOR (cadena) Convierte la cadena cadena a mayúsculas
  • CHAR_LENGTH(cadena) calcula la longitud de la cadena cadena
  • Funciones temporales

Funciones que realizan operaciones relacionadas con el tiempo. Por ejemplo:

  • Cadena de FECHA Analice la cadena de cadena según el formato "yyyy-MM-dd", y el tipo de retorno es SQL Date
  • La cadena TIMESTAMP se analiza de acuerdo con el formato "yyyy-MM-dd HH:mm:ss[.SSS]", y el tipo de devolución es la marca de tiempo SQL
  • CURRENT_TIME devuelve la hora actual en la zona horaria local, el tipo es SQL time (equivalente a LOCALTIME)
  • INTERVALO rango de cadena Devuelve un intervalo de tiempo. la cadena representa un valor; el rango puede ser una unidad como DÍA, MINUTO, DAT A HORA o una unidad compuesta como AÑO A MES. Por ejemplo, "2 años y 10 meses" se puede escribir como:INTERVAL '2-10' YEAR TO MONTH

11.7.1.2 Funciones agregadas

Una función agregada es una función que toma varias filas de una tabla como entrada, extrae campos para operaciones de agregación y, como resultado, devuelve un valor agregado único. Las funciones de agregación se utilizan ampliamente Independientemente de la agregación de grupos, la agregación de ventanas o la agregación de ventanas (sobre), las operaciones de agregación de datos se pueden definir con la misma función.
Las funciones agregadas comunes en SQL estándar son compatibles con Flink SQL y actualmente se están ampliando para proporcionar funciones más potentes para las aplicaciones de procesamiento de flujo. Por ejemplo:

  • COUNT (*) devuelve el número de todas las filas, estadísticas
  • SUM([ ALL | DISTINCT ] expresión) Suma un campo. De forma predeterminada, se omite la palabra clave ALL, lo que indica que se suman todas las filas; si se especifica DISTINCT, los datos se deduplicarán y cada valor solo se superpondrá una vez.
  • RANK() devuelve el rango del valor actual en un conjunto de valores
  • ROW_NUMBER() Después de ordenar un conjunto de valores, devuelve el número de fila del valor actual. Similar a la función de RANK(), RANK() y ROW_NUMBER() generalmente se usan en la ventana OVER.

11.7.1.2, función definida por el usuario (UDF)

Table API y SQL de Flink proporcionan interfaces para varias funciones personalizadas, que se definen en forma de clases abstractas. El UDF actual tiene principalmente las siguientes categorías:

  • Funciones escalares: convierte un valor escalar de entrada en un nuevo valor escalar;
  • Funciones de tabla: convierte valores escalares en uno o más datos de fila nuevos, es decir, expande en una tabla;
  • Funciones agregadas: convierta valores escalares en varias filas de datos en un nuevo valor escalar;
  • Funciones de tabla agregada: convierta valores escalares en varias filas de datos en una o más filas de datos nuevas.
11.7.1.2.1, el proceso de llamada general

Para usar una función personalizada en el código, primero debemos personalizar la implementación de la clase abstracta UDF correspondiente y registrar esta función en el entorno de la tabla, y luego se puede llamar en Table API y SQL.

  1. función de registro

Al registrar una función, debe llamar al método createTemporarySystemFunction() del entorno de la tabla y pasar el nombre de la función registrada y el objeto Class de la clase UDF:

// 注册函数
tableEnv.createTemporarySystemFunction("MyFunction", MyFunction.class);

Nuestra clase UDF personalizada se llama MyFunction, y debe ser una implementación concreta de una de las cuatro clases abstractas de UDF anteriores; regístrela como una función llamada MyFunction en el entorno.

El método createTemporarySystemFunction() aquí significa crear una "función de sistema temporal", por lo que el nombre de función MyFunction es global y puede usarse como una función de sistema; también podemos usar el método createTemporaryFunction(), y la función registrada depende de la función actual. Base de datos (base de datos) y catálogo (catálogo), por lo que esta no es una función del sistema, sino una "función de catálogo". Su nombre completo debe incluir la base de datos y el catálogo al que pertenece. En general, usamos directamente el método createTemporarySystemFunction () para registrar UDF como una función del sistema.

  1. Uso de Table API para llamar a funciones
    En Table API, debe usar el método call() para llamar a funciones personalizadas:
tableEnv.from("MyTable").select(call("MyFunction", $("myField")));

Aquí, el método call() tiene dos parámetros, uno es el nombre de la función registrada MyFunction y el otro es el parámetro en sí cuando se llama a la función. Aquí definimos que cuando se llama a MyFunction, el parámetro que debe pasarse es el campo myField.

Además, en Table API, puede llamar directamente a la UDF en el modo "en línea" sin registrar la función:

tableEnv.from("MyTable").select(call(SubstringFunction.class, $("myField")));

La única diferencia es que el primer parámetro del método call() ya no es el nombre de la función registrada, sino directamente el objeto Class de la clase de función.

  1. Llamar a la función en SQL

Cuando registramos la función como una función del sistema, la llamada en SQL es exactamente la misma que la función del sistema integrada:

tableEnv.sqlQuery("SELECT MyFunction(myField) FROM MyTable");

Se puede ver que el método de llamada de SQL es más conveniente y seguiremos usando SQL como ejemplo para presentar el uso de UDF en el futuro.

11.7.1.2.2 Funciones escalares

Una función escalar personalizada puede convertir 0, 1 o más valores escalares en un valor escalar, y su entrada correspondiente es un campo en una fila de datos, y la salida es un valor único. Por lo tanto, desde la perspectiva de la relación correspondiente entre los datos de fila en las tablas de entrada y salida, la función escalar es una conversión "uno a uno".

Para implementar una función escalar personalizada, necesitamos definir una clase para heredar la clase abstracta ScalarFunction e implementar un método de evaluación llamado eval(). El comportamiento de las funciones escalares depende de la definición del método de evaluación, el cual debe ser público (public), y el nombre debe ser eval. El método de evaluación eval se puede sobrecargar varias veces y se puede usar cualquier tipo de datos como parámetro y tipo de valor de retorno del método de evaluación.

Lo que debe explicarse especialmente aquí es que el método eval() no está definido en la clase abstracta ScalarFunction, por lo que no podemos anularlo directamente en el código; pero la capa inferior del marco de Table API requiere que el método de evaluación se nombre evaluar() .
ScalarFunction y todas las demás interfaces UDF están en org.apache.flink.table.functions. Veamos un ejemplo concreto. Implementamos una función hash personalizada HashFunction para encontrar el valor hash del objeto entrante.

public static class HashFunction extends ScalarFunction {
    
    
 // 接受任意类型输入,返回 INT 型输出
 public int eval(@DataTypeHint(inputGroup = InputGroup.ANY) Object o) {
    
    
 return o.hashCode();
 }
}
// 注册函数
tableEnv.createTemporarySystemFunction("HashFunction", HashFunction.class);
// 在 SQL 里调用注册好的函数
tableEnv.sqlQuery("SELECT HashFunction(myField) FROM MyTable");

Aquí hemos personalizado una ScalarFunction, implementado el método de evaluación eval(), pasado cualquier tipo de objeto y devuelto un valor hash de tipo Int. Por supuesto, se omite la operación hash específica y se puede llamar directamente al método hashCode() del objeto.

También tenga en cuenta que debido a que Table API necesita extraer la referencia de tipo del parámetro del método de evaluación al analizar la función, usamos DataTypeHint(inputGroup = InputGroup.ANY) para marcar el tipo del parámetro de entrada, lo que indica que el parámetro de eval puede ser de cualquier tipo

11.7.1.2.3 Funciones de tabla

Al igual que las funciones escalares, las funciones de tabla pueden tomar cero, uno o más valores escalares como argumentos de entrada; la diferencia es que pueden devolver cualquier cantidad de filas. Los "datos de varias filas" en realidad constituyen una tabla, por lo que la "función de tabla" se puede considerar como una función que devuelve una tabla, que es una relación de conversión de "uno a muchos".

De manera similar, para implementar una función de tabla personalizada, se requiere una clase personalizada para heredar la clase abstracta TableFunction, y se debe implementar internamente un método de evaluación llamado eval. A diferencia de la función escalar, la clase TableFunction en sí tiene un parámetro genérico T, que es el tipo de datos devueltos por la función de tabla; y el método eval() no tiene tipo de retorno, y no hay declaración de retorno dentro, que se obtiene llamando al método collect () para enviar los datos de la fila que desea generar.

FlatMapFunction y ProcessFunction en la API de DataStream, sus métodos flatMap y processElement tampoco tienen valor de retorno, y también envían datos en sentido descendente a través de out.collect().

Para llamar a una función de tabla en SQL, debe usar LATERAL TABLE() para generar una "tabla lateral" extendida y luego unirse a la tabla original. La operación de combinación aquí puede ser una combinación cruzada directa (combinación cruzada), simplemente separe las dos tablas con una coma después de DESDE; también puede ser una combinación izquierda (UNIÓN IZQUIERDA) con EN VERDADERO como condición.

El siguiente es un ejemplo concreto de una función de tabla. Hemos implementado una función SplitFunction que separa cadenas, que puede convertir una cadena en una tupla (cadena, longitud).

// 注意这里的类型标注,输出是 Row 类型,Row 中包含两个字段:word 和 length。
@FunctionHint(output = @DataTypeHint("ROW<word STRING, length INT>"))
public static class SplitFunction extends TableFunction<Row> {
    
    
	 public void eval(String str) {
    
    
		 for (String s : str.split(" ")) {
    
    
			 // 使用 collect()方法发送一行数据
			 collect(Row.of(s, s.length()));
		 }
	 }
}
// 注册函数
tableEnv.createTemporarySystemFunction("SplitFunction", SplitFunction.class);
// 在 SQL 里调用注册好的函数
// 1. 交叉联结
tableEnv.sqlQuery( "SELECT myField, word, length " +
 "FROM MyTable, LATERAL TABLE(SplitFunction(myField))");
// 2. 带 ON TRUE 条件的左联结
tableEnv.sqlQuery(
	 "SELECT myField, word, length " +
	 "FROM MyTable " +
	 "LEFT JOIN LATERAL TABLE(SplitFunction(myField)) ON TRUE");
	// 重命名侧向表中的字段
tableEnv.sqlQuery(
	 "SELECT myField, newWord, newLength " +
	 "FROM MyTable " +
 	"LEFT JOIN LATERAL TABLE(SplitFunction(myField)) AS T(newWord, newLength) ON TRUE");

Aquí definimos directamente el tipo de salida de la función de tabla como ROW, que es el tipo de datos en la tabla lateral obtenida; cada fila de datos tiene solo una fila después de la conversión. Hemos utilizado dos métodos de combinación cruzada y combinación izquierda para llamar en SQL, y también podemos cambiar el nombre de los campos en la tabla lateral.

11.7.1.2.4 Funciones agregadas

Una función AGGregate definida por el usuario (UDAGG) agrega una o más filas de datos (es decir, una tabla) en un valor escalar. Esta es una conversión estándar de "muchos a uno". Hemos encontrado el concepto de funciones agregadas muchas veces antes, como SUM(), MAX(), MIN(), AVG() y COUNT() son sistemas comunes Funciones agregadas incorporadas. Y si algunos requisitos no se pueden resolver llamando directamente a las funciones del sistema, debemos personalizar las funciones de agregación para realizar las funciones.

Una función agregada personalizada debe heredar la clase abstracta AggregateFunction. AggregateFunction tiene dos parámetros genéricos <T, ACC>, T representa el tipo de resultado de la salida de agregación y ACC representa el tipo de estado intermedio de la agregación. Las funciones agregadas en Flink SQL funcionan de la siguiente manera:

  • Primero, necesita crear un acumulador (acumulador), utilizado para almacenar los resultados intermedios de la agregación. Esto es muy similar a AggregateFunction en la API de DataStream, y el acumulador se puede considerar como un estado agregado. Llame al método createAccumulator() para crear un acumulador vacío.
  • Para cada fila de datos de entrada, se llama al método de acumulación () para actualizar el acumulador, que es el proceso central de agregación.
  • Cuando se han procesado todos los datos, el resultado final se calcula y se devuelve llamando al método getValue().

Por lo tanto, cada AggregateFunction debe implementar los siguientes métodos:

  • createAccumulator()
    Este es el método para crear el acumulador. Sin parámetros de entrada, el tipo de retorno es el tipo de acumulador ACC.

  • acumula()
    es el método central para el cálculo agregado, y se llamará para cada fila de datos. Su primer parámetro es definitivo, es decir, el acumulador actual, cuyo tipo es ACC, que indica el estado intermedio de la agregación actual; el último parámetro es el parámetro que se pasa cuando se llama a la función de agregación, puede haber múltiples y los tipos también puede ser diferente. Este método es principalmente para actualizar el estado agregado, por lo que no hay ningún tipo de devolución. Debe tenerse en cuenta que Accumul() es similar al método de evaluación anterior eval(), y también es requerido por la arquitectura subyacente. Debe ser público, y el nombre del método debe ser acumular, y no puede anularse directamente y puede solo puede implementarse manualmente.

  • getValue()
    Este es el método para obtener el resultado de retorno final. El parámetro de entrada es un acumulador de tipo ACC y el tipo de salida es T. Al encontrar tipos complejos, es posible que la deducción de tipos de Flink no obtenga resultados correctos. Entonces AggregateFunction también puede declarar específicamente el tipo del acumulador y el resultado devuelto, que se especifica mediante los dos métodos getAccumulatorType() y getResultType().

Además de los métodos anteriores, varios métodos son opcionales. Algunos de estos métodos pueden hacer que las consultas sean más eficientes y algunos deben implementarse en determinados escenarios. Por ejemplo, si se agrega la ventana de sesión, se debe implementar el método merge(), que definirá la operación de fusión del acumulador, y este método también es útil para la optimización de algunos escenarios; y si se usa la función de agregación en la agregación de ventana OVER, el método retract() debe implementarse para garantizar que los datos se puedan retirar;

El método resetAccumulator() reinicia el acumulador, lo cual es útil en algunos escenarios de procesamiento por lotes. Todos los métodos de AggregateFunction deben ser públicos (public), no estáticos (static), y los nombres deben ser exactamente como se escriben arriba. Los métodos createAccumulator, getValue, getResultType y getAccumulatorType están definidos en la clase abstracta AggregateFunction y pueden anularse, mientras que otros son métodos acordados por la arquitectura subyacente.

Por ejemplo, queremos calcular la puntuación media ponderada de cada alumno a partir de la tabla de puntuaciones ScoreTable del alumno. Para calcular un promedio ponderado, se deben extraer dos valores de cada fila de entrada como parámetros: el puntaje a calcular y su peso. En el proceso de agregación, el acumulador (acumulador) necesita almacenar la suma de la suma ponderada actual y el número actual de conteo de datos. Esto se puede representar mediante una tupla de dos, o puede definir una clase WeightedAvgAccum por separado, que contiene dos atributos de suma y recuento, y usar su instancia de objeto como el acumulador agregado. El código específico es el siguiente:

// 累加器类型定义
public static class WeightedAvgAccumulator {
    
    
 public long sum = 0; // 加权和
 public int count = 0; // 数据个数
}
// 自定义聚合函数,输出为长整型的平均值,累加器类型为 WeightedAvgAccumulator
public static class WeightedAvg extends AggregateFunction<Long, WeightedAvgAccumulator> {
    
    
	 @Override
	 public WeightedAvgAccumulator createAccumulator() {
    
    
	 		return new WeightedAvgAccumulator(); // 创建累加器
	 }
	 @Override
	 public Long getValue(WeightedAvgAccumulator acc) {
    
    
		 if (acc.count == 0) {
    
    
		 	return null; // 防止除数为 0
		 } else {
    
    
			 return acc.sum / acc.count; // 计算平均值并返回
		 }
 	}
 // 累加计算方法,每来一行数据都会调用
 public void accumulate(WeightedAvgAccumulator acc, Long iValue, Integer iWeight) {
    
    
			 acc.sum += iValue * iWeight;
			 acc.count += iWeight;
		 }
	}
// 注册自定义聚合函数
tableEnv.createTemporarySystemFunction("WeightedAvg", WeightedAvg.class);
// 调用函数计算加权平均值
Table result = tableEnv.sqlQuery("SELECT student, WeightedAvg(score, weight) FROM ScoreTable GROUP BY student" );

El método de acumulación () de una función agregada tiene tres parámetros de entrada. El primero es un acumulador de tipo WeightedAvgAccum, los otros dos son campos de entrada cuando se llama a la función: el valor a calcular ivalue y el peso correspondiente iweight.

11.7.1.2.5 Funciones de tabla agregada

Una función de agregación de tablas personalizada debe heredar la clase abstracta TableAggregateFunction. La estructura y el principio de TableAggregateFunction son muy similares a los de AggregateFunction, además tiene dos parámetros genéricos <T, ACC>, y utiliza un acumulador de tipo ACC (acumulador) para almacenar los resultados intermedios de la agregación. Los tres métodos que deben implementarse en la función de agregación también deben implementarse en TableAggregateFunction:

  • createAccumulator()
    es el método para crear un acumulador, que es el mismo que se usa en AggregateFunction.
  • acumular()
    es el método principal de cálculo de agregación, que es el mismo que se usa en AggregateFunction.
  • emitValue()
    es un método para generar el resultado del cálculo final después de que se hayan procesado todas las filas de entrada. Este método corresponde al método getValue() en AggregateFunction; la diferencia es que emitValue no tiene tipo de salida y hay dos parámetros de entrada: el primero es un acumulador de tipo ACC y el segundo es un "recolector" para datos de salida , su tipo es Collect. Entonces, obviamente, los datos de salida de la función de agregación de tablas no se devuelven directamente, sino que llaman al método out.collect(), y se pueden generar múltiples filas de datos llamándolos varias veces; esto es muy similar a la función de tabla. Además, emitValue() no está definido en la clase abstracta, por lo que no se puede anular y debe implementarse manualmente.

Lo que obtiene la función de agregación de tablas es una tabla; si realiza una consulta continua en el procesamiento de secuencias, debe volver a calcular la salida de esta tabla cada vez. Si después de ingresar un dato, solo se actualizan una o varias filas en la tabla de resultados (Actualizar), entonces obviamente no es lo suficientemente eficiente como para que recalculemos toda la tabla y las generemos todas. Para mejorar la eficiencia del procesamiento, TableAggregateFunction también proporciona un método emitUpdateWithRetract(), que puede actualizarse de forma incremental "retrayendo" los datos antiguos y enviando datos nuevos cuando cambia la tabla de resultados. Si se definen los métodos emitValue() y emitUpdateWithRetract(), se llamará primero a emitUpdateWithRetract() al actualizar.

Las funciones de agregación de tablas son relativamente complejas y un escenario de aplicación típico son las N consultas principales. Por ejemplo, queremos seleccionar los dos primeros después de ordenar un conjunto de datos, que es la consulta TOP-2 más simple. Si no hay una función de sistema de subprocesos, podemos personalizar una función de agregación de tablas para lograr esta función. El acumulador debería poder guardar los dos valores actuales más grandes. Cada vez que llegue un nuevo dato, compárelo y actualícelo en el método de acumulación (), y finalmente llame a out.collect () dos veces en emitValue () para recopilar los dos primeros salida de datos El código específico es el siguiente:

// 聚合累加器的类型定义,包含最大的第一和第二两个数据
public static class Top2Accumulator {
    
    
 public Integer first;
 public Integer second;
}
// 自定义表聚合函数,查询一组数中最大的两个,返回值为(数值,排名)的二元组
public static class Top2 extends TableAggregateFunction<Tuple2<Integer, Integer>, 
Top2Accumulator> {
    
    
	 @Override
	 public Top2Accumulator createAccumulator() {
    
    
		 Top2Accumulator acc = new Top2Accumulator();
		 acc.first = Integer.MIN_VALUE; // 为方便比较,初始值给最小值
		 acc.second = Integer.MIN_VALUE;
		 return acc;
	 }
 // 每来一个数据调用一次,判断是否更新累加器
 public void accumulate(Top2Accumulator acc, Integer value) {
    
    
	 if (value > acc.first) {
    
    
		 acc.second = acc.first;
		 acc.first = value;
	 } else if (value > acc.second) {
    
    
		 acc.second = value;
	 }
 }
 // 输出(数值,排名)的二元组,输出两行数据
 public void emitValue(Top2Accumulator acc, Collector<Tuple2<Integer, Integer>> out) {
    
    
		 if (acc.first != Integer.MIN_VALUE) {
    
    
		 	out.collect(Tuple2.of(acc.first, 1));
		 }
		 if (acc.second != Integer.MIN_VALUE) {
    
    
		 	out.collect(Tuple2.of(acc.second, 2));
		 }
	 }
}

Actualmente, no hay forma de usar directamente las funciones de agregación de tablas en SQL, por lo que debe usar Table API para llamar:

// 注册表聚合函数函数
tableEnv.createTemporarySystemFunction("Top2", Top2.class);
// 在 Table API 中调用函数
tableEnv.from("MyTable")
 .groupBy($("myField"))
 .flatAggregate(call("Top2", $("value")).as("value", "rank"))
 .select($("myField"), $("value"), $("rank"));

Aquí se utiliza el método flatAggregate(), que es una interfaz especialmente utilizada para llamar a funciones de agregación de tablas. Agrupe y agregue los datos en MyTable de acuerdo con el campo myField, cuente los dos con el valor más grande, cambie el nombre de los dos campos del resultado de la agregación a value y rank, y luego use select() para extraerlos.

11.9 Conexión a sistemas externos

11.9.1, Kafka

El conector SQL de Kafka puede leer datos del tema de Kafka (tema) en una tabla y también puede escribir datos de tabla en el tema de Kafka. En otras palabras, si especifica el conector como Kafka al crear una tabla, la tabla se puede usar como tabla de entrada y como tabla de salida.

11.9.1.1 Introducción de dependencias

Para usar el conector Kafka en un programa Flink, se deben introducir las siguientes dependencias:

<dependency>
 <groupId>org.apache.flink</groupId>
 <artifactId>flink-connector-kafka_${
    
    scala.binary.version}</artifactId>
 <version>${
    
    flink.version}</version>
</dependency>

Los conectores Flink y Kafka que presentamos aquí son los mismos que los conectores introducidos en la API de DataStream anterior. Si desea utilizar el conector Kafka en el cliente SQL, también debe descargar el paquete jar correspondiente y colocarlo en el directorio lib. Además, Flink proporciona una serie de "formatos de tabla" para varios conectores, como CSV, JSON, Avro, Parquet, etc. Estos formatos de tabla definen el método de conversión entre los datos binarios almacenados en la capa inferior y las columnas de la tabla, que es equivalente a la herramienta de serialización de la tabla. Para Kafka, se admiten los principales formatos, como CSV, JSON y Avro. Según el formato configurado en el conector de Kafka, es posible que debamos introducir la compatibilidad con las dependencias correspondientes. Tome CSV como ejemplo:

<dependency>
 <groupId>org.apache.flink</groupId>
 <artifactId>flink-csv</artifactId>
 <version>${
    
    flink.version}</version>
</dependency>

Dado que el cliente SQL tiene soporte integrado para CSV y JSON, no es necesario introducirlo al usarlo; para formatos sin soporte integrado (como Avro), aún necesita descargar el paquete jar correspondiente.

11.9.1.2 Crear una tabla conectada a Kafka

Para crear una conexión a una tabla de Kafka, debe especificar el conector como Kafka en la cláusula WITH en el DDL de CREATE TABLE y definir los parámetros de configuración necesarios. He aquí un ejemplo concreto:

CREATE TABLE KafkaTable (
`user` STRING,
 `url` STRING,
 `ts` TIMESTAMP(3) METADATA FROM 'timestamp'
) WITH (
 'connector' = 'kafka',
 'topic' = 'events',
 'properties.bootstrap.servers' = 'localhost:9092',
 'properties.group.id' = 'testGroup',
 'scan.startup.mode' = 'earliest-offset',
 'format' = 'csv'
)

Esto define el tema (tema), el servidor de Kafka, el ID del grupo de consumidores, el modo de inicio del consumidor y el formato de la tabla correspondiente al conector de Kafka. Cabe señalar que hay un ts en el campo de KafkaTable, y en su declaración se usa METADATA FROM, lo que significa una "columna de metadatos", que se compone de la "marca de tiempo" de los metadatos del conector Kafka generado. La marca de tiempo aquí es en realidad la marca de tiempo que viene con los datos en Kafka. La extraemos directamente como metadatos y la convertimos en un nuevo campo ts.

11.9.1.3, Actualizar Kafka

En circunstancias normales, Kafka es una cola de mensajes que mantiene el orden de los datos. Tanto la lectura como la escritura deben transmitir datos, lo que corresponde al modo de solo agregar en la tabla. Si queremos escribir la tabla de resultados con una operación de actualización (como agrupación y agregación) en Kafka, provocará una excepción porque Kafka no puede reconocer el mensaje de retracción (retract) o actualización (upsert).

Para solucionar este problema, Flink agrega especialmente un conector "Upsert Kafka" (Upsert Kafka). Este conector admite la lectura y escritura de datos en el tema de Kafka en forma de inserción de actualización (UPSERT). Específicamente, el conector Upsert Kafka maneja flujos de registro de cambios (changlog). Si se usa como TableSource, el conector interpretará los datos leídos (clave, valor) en el tema como una actualización (UPDATE) del valor de datos de la clave actual, es decir, para encontrar una fila de datos correspondiente a la clave en la tabla dinámica y convierta el valor Actualizar al último valor; debido a que es una operación Upsert, si no hay una fila correspondiente a la clave, también se realizará la operación INSERT. Además, si el valor está vacío (nulo), el conector interpreta este dato como una operación DELETE en la fila correspondiente a la clave.

Si se usa como TableSink, el conector Upsert Kafka convertirá la tabla de resultados de la operación de actualización en una secuencia de registro de cambios. Si encuentra datos que se insertan (INSERTAR) o actualizan (UPDATE_AFTER), corresponden a un mensaje de agregar (agregar), entonces normalmente se escriben directamente en el tema de Kafka; si se eliminan (ELIMINAR) o los datos antes de la actualización, corresponde a Retirar (retractar) el mensaje, luego escribir los datos cuyo valor está vacío (nulo) a Kafka. Dado que Flink divide los datos según el valor de la clave (clave), puede garantizar que los mensajes de actualización y eliminación en la misma clave caigan en la misma partición.
Este es un ejemplo de creación y uso de una tabla Upsert Kafka:

CREATE TABLE pageviews_per_region (
 user_region STRING,
 pv BIGINT,
 uv BIGINT,
 PRIMARY KEY (user_region) NOT ENFORCED
) WITH (
 'connector' = 'upsert-kafka',
 'topic' = 'pageviews_per_region',
 'properties.bootstrap.servers' = '...',
 'key.format' = 'avro',
 'value.format' = 'avro'
);
CREATE TABLE pageviews (
 user_id BIGINT,
 page_id BIGINT,
 viewtime TIMESTAMP,
 user_region STRING,
 WATERMARK FOR viewtime AS viewtime - INTERVAL '2' SECOND
) WITH (
 'connector' = 'kafka',
 'topic' = 'pageviews',
 'properties.bootstrap.servers' = '...',
 'format' = 'json'
);
-- 计算 pv、uv 并插入到 upsert-kafka 表中
INSERT INTO pageviews_per_region
SELECT
 user_region,
 COUNT(*),
 COUNT(DISTINCT user_id)
FROM pageviews
GROUP BY user_region;

Aquí leemos los datos de las vistas de página de la tabla de Kafka y contamos el PV (todas las vistas) y UV (eliminación de datos duplicados de usuarios) de cada área. Esta es una consulta de actualización para agrupar y agregar, y la tabla resultante actualizará continuamente los datos.

Para escribir la tabla de resultados en el tema pageviews_per_region de Kafka, definimos una tabla Upsert Kafka, que debe usar PRIMARY KEY para especificar la clave principal en su campo, y especificar el formato de serialización de la clave y el valor en la cláusula WITH.

11.9.2, sistema de archivos

Otro tipo de sistema externo muy común es el sistema de archivos (File System). Flink proporciona un conector de sistema de archivos que admite la lectura y escritura de datos de sistemas de archivos locales o distribuidos. Este conector está integrado en Flink, por lo que no se requieren dependencias adicionales para usarlo.
Aquí hay un ejemplo de conexión a un sistema de archivos:

CREATE TABLE MyTable (
 column_name1 INT,
 column_name2 STRING,
 ...
 part_name1 INT,
 part_name2 STRING
) PARTITIONED BY (part_name1, part_name2) WITH (
 'connector' = 'filesystem', -- 连接器类型
 'path' = '...', -- 文件路径
 'format' = '...' -- 文件格式
)

Aquí PARTITIONED BY se usa antes de WITH para particionar los datos. Los conectores del sistema de archivos admiten el acceso a archivos de partición.

11.9.3, JDBC

La tabla de datos relacionales en sí es donde se aplica SQL inicialmente, por lo que también esperamos poder leer y escribir datos de la tabla directamente en la base de datos relacional. El conector JDBC proporcionado por Flink puede leer y escribir datos en cualquier base de datos relacional a través del controlador JDBC (controlador), como MySQL, PostgreSQL, Derby, etc.
Al escribir datos en la base de datos como TableSink, el modo de operación depende de si el DDL que crea la tabla define una clave principal (primary key). Si hay una clave principal, el conector JDBC se ejecutará en modo Upsert y puede enviar operaciones de ACTUALIZACIÓN y ELIMINACIÓN a la base de datos externa de acuerdo con la clave especificada (clave); si no hay una clave principal definida, se ejecutará en Anexar modo, no admite operaciones de actualización y eliminación.

11.9.3.1 Introducción de dependencias

Para usar el conector JDBC en un programa Flink, se deben introducir las siguientes dependencias:

<dependency>
 <groupId>org.apache.flink</groupId>
 <artifactId>flink-connector-jdbc_${
    
    scala.binary.version}</artifactId>
 <version>${
    
    flink.version}</version>
</dependency>

Además, para conectarnos a una base de datos específica, también importamos dependencias de controladores relacionadas, como MySQL:

<dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 <version>5.1.38</version>
</dependency>

La versión del controlador presentada aquí es 5.1.38, los lectores pueden elegir según su propia versión de MySQL.

11.9.3.2 Crear tabla JDBC

El método para crear una tabla JDBC es similar al Upsert Kafka anterior. He aquí un ejemplo concreto:

-- 创建一张连接到 MySQL 的 表
CREATE TABLE MyTable (
 id BIGINT,
 name STRING,
 age INT,
 status BOOLEAN,
 PRIMARY KEY (id) NOT ENFORCED
) WITH (
 'connector' = 'jdbc',
 'url' = 'jdbc:mysql://localhost:3306/mydatabase',
 'table-name' = 'users'
);
-- 将另一张表 T 的数据写入到 MyTable 表中
INSERT INTO MyTable
SELECT id, name, age, status FROM T;

Aquí, la clave principal se define en el DDL de la creación de la tabla, por lo que los datos se escribirán en la tabla MySQL en modo Upsert; y la conexión a MySQL se define a través de la url en la cláusula WITH. Cabe señalar que el nombre real de la tabla escrito en MySQL es usuarios, y MyTable es una tabla registrada en el entorno de tablas de Flink.

11.9.4、Búsqueda elástica

Como motor de búsqueda y análisis distribuido, Elasticsearch tiene muchos escenarios en aplicaciones de big data. El conector SQL de Elasticsearch proporcionado por Flink solo se puede usar como TableSink, que puede escribir datos de tabla en el índice de Elasticsearch (índice). El uso del conector Elasticsearch es muy similar al del conector JDBC.El modo de escritura de datos también está determinado por si existe una definición de clave principal en el DDL para crear la tabla.

11.9.4.1 Introducción de dependencias

Para usar el conector Elasticsearch en un programa Flink, debe introducir las dependencias correspondientes. Las dependencias específicas están relacionadas con la versión del servidor de Elasticsearch. Para la versión 6.x, las dependencias se introducen de la siguiente manera:

<dependency>
 <groupId>org.apache.flink</groupId> 
<artifactId>flink-connector-elasticsearch6_${
    
    scala.binary.version}</artifactId>
<version>${
    
    flink.version}</version>
</dependency>

Para versiones superiores a Elasticsearch 7, las dependencias introducidas son:

<dependency>
 <groupId>org.apache.flink</groupId> 
<artifactId>flink-connector-elasticsearch7_${
    
    scala.binary.version}</artifactId>
<version>${
    
    flink.version}</version>
</dependency>

11.9.4.2 Crear una tabla conectada a Elasticsearch

El método para crear una tabla de Elasticsearch es básicamente el mismo que el de una tabla JDBC. He aquí un ejemplo concreto:

-- 创建一张连接到 Elasticsearch 的 表
CREATE TABLE MyTable (
 user_id STRING,
 user_name STRING
 uv BIGINT,
 pv BIGINT,
 PRIMARY KEY (user_id) NOT ENFORCED
) WITH (
 'connector' = 'elasticsearch-7',
 'hosts' = 'http://localhost:9200',
 'index' = 'users'
);

La clave principal se define aquí, por lo que los datos se escribirán en Elasticsearch en modo Upsert.

11.9.5, base H

Como base de datos de almacenamiento en columnas distribuidas, escalable y de alto rendimiento, HBase es una herramienta muy importante en el análisis de big data. El conector HBase proporcionado por Flink admite operaciones de lectura y escritura para clústeres HBase.

En el escenario de procesamiento de secuencias, cuando el conector escribe datos en HBase como TableSink, siempre adopta el modo de inserción de actualización (Upsert). En otras palabras, HBase requiere que el conector pase la clave principal definida (clave principal) para enviar el registro de cambios del registro de actualización). Por lo tanto, en el DDL de creación de una tabla, debemos definir el campo de clave de fila (rowkey) y declararlo como la clave principal; si la clave principal no se declara con la cláusula PRIMARY KEY, el conector tendrá por defecto la clave de fila como clave principal. .

11.9.5.1 Introducción de dependencias

Si desea utilizar el conector HBase en el programa Flink, debe introducir las dependencias correspondientes. En la actualidad, Flink solo proporciona soporte de conector para las versiones HBase 1.4.x y 2.2.x, y las dependencias introducidas también deben estar relacionadas con versiones específicas de HBase. Para la versión 1.4, las dependencias introducidas son las siguientes:

<dependency>
 <groupId>org.apache.flink</groupId>
 <artifactId>flink-connector-hbase-1.4_${
    
    scala.binary.version}</artifactId>
 <version>${
    
    flink.version}</version>
</dependency>

Para la versión HBase 2.2, las dependencias introducidas son:

<dependency>
 <groupId>org.apache.flink</groupId>
 <artifactId>flink-connector-hbase-2.2_${
    
    scala.binary.version}</artifactId>
 <version>${
    
    flink.version}</version>
</dependency>

11.9.5.2 Crear una tabla conectada a HBase

Dado que HBase no es una base de datos relacional, será un poco problemático convertirlo en una tabla en Flink SQL. En la tabla HBase creada por DDL, todas las familias de columnas (column family) deben declararse como tipo ROW, ocupando un campo en la tabla; y la columna (calificador de columna) en cada familia corresponde al campo anidado en ROW. No necesitamos declarar toda la familia y el calificador en HBase en la tabla SQL de Flink, solo declarar los que se usan en la consulta. Además de todos los campos de tipo ROW (correspondientes a la familia en HBase), debe haber un campo de tipo atómico en la tabla, que se reconocerá como la clave de fila de HBase. En la tabla, este campo se puede nombrar arbitrariamente y no tiene que llamarse clave de fila.

He aquí un ejemplo concreto:

-- 创建一张连接到 HBase 的 表
CREATE TABLE MyTable (
rowkey INT,
family1 ROW<q1 INT>,
family2 ROW<q2 STRING, q3 BIGINT>,
family3 ROW<q4 DOUBLE, q5 BOOLEAN, q6 STRING>,
PRIMARY KEY (rowkey) NOT ENFORCED
) WITH (
'connector' = 'hbase-1.4',
'table-name' = 'mytable',
'zookeeper.quorum' = 'localhost:2181'
);

-- 假设表 T 的字段结构是 [rowkey, f1q1, f2q2, f2q3, f3q4, f3q5, f3q6]
INSERT INTO MyTable
SELECT rowkey, ROW(f1q1), ROW(f2q2, f2q3), ROW(f3q4, f3q5, f3q6) FROM T;

Extraemos los datos de otra T y usamos la función ROW() para construir la familia de columnas correspondiente y finalmente la escribimos en la tabla llamada mytable en HBase.

Supongo que te gusta

Origin blog.csdn.net/prefect_start/article/details/129570461
Recomendado
Clasificación