Flink SQL踩坑记录-IF函数

问题背景

业务需要将部分离线数仓看板改实时,选择Flink SQL脚本实现,改写Hive SQL过程中有两个关于IF函数的报错点。

问题代码一

select count(distinct if(order_cnt>0,uid,null)) from table

由于是从hive sql直接粘过来开始改,开始保留了这部分,报错信息为

org.apache.calcite.sql.validate.SqlValidatorException: Illegal use of ‘NULL’ at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at

原因

  • Hive中语法较为宽松,NULL会被隐式处理,IF()函数返回值的类型由两个分支自定推导,自动将NULL处理为order_cnt同类型的NULL,这样写不会报错。

  • Flink SQL 使用 Apache Calcite 作为 SQL 解析和验证引擎,Calcite 对表达式中的 NULL 类型推断非常严格,尤其关注所有分支的类型一致性

Flink IF 函数的类型推导

  • IF(condition, expr1, expr2) 的三个参数必须满足以下条件: condition 必须是布尔类型。
  • expr1 和 expr2 必须具有完全相同的数据类型(包括 NULL 的类型)。
    如果 expr2 是裸的 NULL(即未显式指定类型的 NULL),Calcite 无法自动推断其类型,会认为它是一个“未知类型”的 NULL,导致与 expr1 类型冲突。

解决办法

#Calcite 对 CASE WHEN 的类型推导更智能,更容易正确处理 NULL 的类型匹配
COUNT(DISTINCT CASE WHEN order_cnt > 0 THEN uid ELSE NULL END)
#强转 显式声明
COUNT(DISTINCT IF(order_cnt > 0, uid, CAST(NULL AS STRING)))

问题代码二

select  
    if(grouping(category_name1) = 1 ,'ALL',category_name1) as category_name1,
    if(grouping(category_name2) = 1 ,'ALL',category_name2) as category_name2,
    if(grouping(category_name3) = 1 ,'ALL',category_name3) as category_name3,
    if(grouping(category_id1) = 1 ,'ALL',category_id1) as category_id1,
    if(grouping(category_id2) = 1 ,'ALL',category_id2) as category_id2,
    if(grouping(category_id3) = 1 ,'ALL',category_id3) as category_id3,
    sum(indicator) as indicator_cnt,
from
    table 
where category_name1 is not null and category_name2 is not null and category_name3 is not null 
group by
grouping sets ((),--整体汇总
            (category_name1,category_id1), --类目1汇总
            (category_name1,category_name2,category_id1,category_id2), --类目2汇总
            (category_name1,category_name2,category_name3,category_id1,category_id2,category_id3),--类目3汇总 即明细
        ) 

看起来没什么问题,但是Sink输出的category_name1等不定长度字段长度都被截取为3不符合预期,单独验证上游sink结果正常后定位到此处。

原因

  • 前面提到的Calcite严格要求 IF表达式的所有分支类型必须一致,“ALL” 字符串,默认推导为 CHAR(3),category_name1原有类型为STRING是可变长度类型,系统会选择 CHAR(3) 作为共同类型。
  • Calcite 的类型推导遵循优先级规则:CHAR(n) > VARCHAR(n) > STRING
    当遇到类型冲突时低优先级类型(如 VARCHAR)会被强制转换为高优先级类型(如 CHAR)

解决办法

-- 强制 'ALL' 为 VARCHAR
IF(
  grouping(category_name1) = 1, 
  CAST('ALL' AS VARCHAR(3)),
  category_name1
) AS category_name1

扩展

Calcite 的严格类型推导规则与Hive中差异,提前记录避免踩坑

一、类型一致性问题

  1. 条件表达式(IF/CASE)的分支类型不一致
  • Hive:允许分支类型不同(例如数字和字符串),隐式统一为 STRING。
  • Flink (Calcite):强制要求所有分支类型严格一致。
-- Hive 正常,Flink 报错 ❌
SELECT IF(condition, 123, 'abc') 

-- 修正 ✅
SELECT IF(condition, CAST(123 AS STRING), 'abc')

  1. 空字符串与 NULL 的差异
  • Hive:‘’(空字符串)和 NULL 可能被隐式转换。
  • Flink:严格区分 VARCHAR 空字符串与 NULL,需显式处理。
-- Hive 允许,Flink 报类型不匹配 ❌
SELECT IF(condition, '', NULL) 

-- 修正 ✅
SELECT IF(condition, CAST('' AS VARCHAR), CAST(NULL AS VARCHAR))

二、数值类型的隐式转换

  1. 整数与浮点数的混合运算
    Hive:自动将整数提升为浮点数(如 INT → DOUBLE)。
    Flink:需显式转换否则可能报错。
-- Hive 返回 DOUBLE,Flink 报错 ❌ (若无显式转换)
SELECT 1 + 2.5

-- 修正 ✅
SELECT CAST(1 AS DOUBLE) + 2.5
  1. Bigint与Integer的溢出风险
  • Hive:可能隐式转换(如 INT → BIGINT)。
  • Flink:严格检查类型与精度,直接拒绝可能导致溢出的操作。
-- Hive 允许,Flink 拒绝(例如 Integer.MAX_VALUE + 1)
SELECT 2147483647 + 1 

-- 修正 ✅(显式转为 BIGINT)
SELECT CAST(2147483647 AS BIGINT) + 1

三、字符串类型和定长类型

  1. CHAR(n) 与 VARCHAR(n) 的隐式填充
  • Hive:忽略定长类型差异,自动处理存储。
  • Flink:CHAR(n) 类型会用空格填充到定长,可能导致比较和连接结果与 Hive 不同。
-- Hive 中返回 TRUE,Flink 中返回 FALSE ❗
SELECT CAST('abc' AS CHAR(5)) = 'abc'

-- Hive 结果: 'abc  '   (填充空格)
-- Flink 比较时会保留填充符,可能不匹配

四、日期和时间的处理

  1. 字符串与日期类型的隐式转换
  • Hive:支持 ‘2023-10-01’ 直接比较 DATE 类型。
  • Flink:需显式转换字符串为日期类型。
-- Hive 正常,Flink 报错 ❌
SELECT DATE '2023-10-01' = '2023-10-01'

-- 修正 ✅
SELECT DATE '2023-10-01' = TO_DATE('2023-10-01')
  1. 时间戳精度差异
  • Hive:默认时间戳精度为秒或毫秒。
  • Flink:时间戳精度为 TIMESTAMP(3)(毫秒),直接使用纳秒可能导致截断或报错。
-- Hive 允许,Flink 可能警告或截断 ❗
SELECT CAST('2023-10-01 12:00:00.123456789' AS TIMESTAMP)

五、复杂类型的处理差异

  1. MAP 或 ARRAY 类型的构造
  • Hive:允许混合不同类型的元素(如 ARRAY(1, ‘a’))。
  • Flink:要求所有元素类型必须严格一致。
-- Hive 返回 ARRAY<STRING>,Flink 报错 ❌
SELECT ARRAY(1, 'a')

-- 修正 ✅
SELECT ARRAY(CAST(1 AS STRING), 'a')
  1. STRUCT 类型的字段顺序
  • Hive:可通过别名访问字段,忽略结构体顺序。
  • Flink:通过索引定位字段,若结构体字段顺序改变可能导致错误。
-- 假设定义 STRUCT<age INT, name STRING>
-- Hive 允许,Flink 可能返回错误值 ❌
SELECT named_struct('name', 'Alice', 'age', 25).age 

六、其他易错点
10. 函数参数类型严格性

  • Hive:允许 CONCAT(1, ‘a’)(自动转字符串)。
  • Flink:要求所有参数类型一致。
-- Hive 返回 '1a',Flink 报错 ❌
SELECT CONCAT(1, 'a')

-- 修正 ✅
SELECT CONCAT(CAST(1 AS STRING), 'a')
  1. ORDER BY 中的混合类型
  • Hive:允许不同字段类型参与排序。
  • Flink:若排序字段类型不一致可能报错。
-- Hive 正常,Flink 可能拒绝 ❌
SELECT id, name FROM table ORDER BY id, LENGTH(name)

总结

  1. 显式指定类型:
    使用 CAST 强制转换,尤其是对 NULL、字面量。
  2. 优先使用标准 SQL:
    用 CASE WHEN 替代 IF(),用 TO_DATE() 替代隐式转换。
  3. 统一分支类型:
    确保 IF/CASE 每个分支、集合元素、函数参数类型一致。
  4. 检查时间精度:
    明确 TIMESTAMP 精度,避免隐式截断。
  5. 测试边缘场景:
    对数值溢出、空字符串、异构集合进行严格测试。
<think>我们正在讨论Flink中的CROSSJOINLATERAL的使用。用户要求一个使用示例。根据Flink官方文档,CROSSJOINLATERAL(也称为表函数连接)用于将表与表函数的结果进行连接。表函数是一种用户自定义函数,它可以返回多行结果(而标量函数只返回单个值)。步骤:1.首先需要定义一个表函数(TableFunction),它可以是继承自`TableFunction`类的自定义函数。2.在SQL查询中,使用`CROSSJOINLATERAL`(或`LEFTJOINLATERAL`)来连接主表和表函数的结果。示例场景:假设我们有一个订单表(orders),每个订单有一个包含多个产品的ID列表(用逗号分隔的字符串)。我们还有一个表函数(split)用于将字符串拆分成多行(即每个产品一行)。然后我们想将每个订单拆分成多行,每行对应订单中的一个产品。步骤分解:a.定义表函数:这里我们使用内置的或自定义的表函数Flink内置了一些函数,但拆分字符串并返回多行的函数可能需要自定义。b.在SQL中使用LATERALJOIN。注意:在FlinkSQL中,表函数连接有两种写法:-使用`CROSSJOINLATERAL`:如果表函数返回空,则不会生成该行的任何结果。-使用`LEFTJOINLATERAL`:即使表函数返回空,也会保留主表的行,并用空值填充表函数的结果列。下面给出一个完整的示例,包括自定义表函数和在SQL中使用。自定义表函数(Scala示例):```scalaimportorg.apache.flink.table.annotation.DataTypeHintimportorg.apache.flink.table.annotation.FunctionHintimportorg.apache.flink.table.functions.TableFunctionimportorg.apache.flink.types.Row//自定义表函数,用于拆分字符串@FunctionHint(output=newDataTypeHint("ROW<productSTRING>"))classSplitFunctionextendsTableFunction[Row]{defeval(str:String,delimiter:String=","):Unit={if(str!=null){str.split(delimiter).foreach{s=>collect(Row.of(s))}}}}```在FlinkTableAPI中注册并使用:```scalaimportorg.apache.flink.table.api._importorg.apache.flink.table.api.bridge.scala._//创建表环境valenv=StreamExecutionEnvironment.getExecutionEnvironmentvaltableEnv=StreamTableEnvironment.create(env)//注册表函数tableEnv.createTemporarySystemFunction("split",newSplitFunction)//假设有一个订单表,包含字段:order_id,products//创建订单表(这里使用虚拟数据)tableEnv.executeSql("""|CREATETABLEorders(|order_idSTRING,|productsSTRING|)WITH(|'connector'='datagen',|'number-of-rows'='3'|)""".stripMargin)//使用CROSSJOINLATERAL拆分每个订单的产品列表valresult=tableEnv.sqlQuery("""|SELECTorder_id,product|FROMorders,|LATERALTABLE(split(products))AST(product)""".stripMargin)//打印结果result.execute().print()```注意:在SQL中,我们使用了`LATERALTABLE(split(products))`,这相当于`CROSSJOINLATERAL`。也可以显式地写成:`FROMordersCROSSJOINLATERALTABLE(split(products))AST(product)`另外,如果使用LEFTJOINLATERAL,则即使拆分结果为空,订单也会被保留:```sqlSELECTorder_id,productFROMordersLEFTJOINLATERALTABLE(split(products))AST(product)ONTRUE```注意:在LEFTJOINLATERAL中,必须加上`ONTRUE`,因为这是一个横向连接,连接条件始终为真(连接条件由表函数的输出行决定,但这里我们只是将表函数应用到每一行)。以上示例展示了如何将每个订单的产品字符串拆分成多行,每行一个产品。相关问题:1.表函数连接与普通连接有什么区别?2.在FlinkSQL中,除了表函数,还有其他类型的函数吗?3.如何处理表函数返回空值的情况?4.在流处理模式下,表函数连接会产生什么样的输出?(例如,当订单表是流时)</think>### Apache Flink CROSS JOIN LATERAL 使用示例 在 Apache Flink 中,`CROSS JOIN LATERAL` 用于将表与表函数(Table Function)的结果进行连接。这种连接方式允许为输入表的每一行调用表函数,并将函数输出的多行结果与原始行连接。以下是详细的使用示例: #### 1. **场景说明** 假设有两个数据源: - **订单表** `Orders(order_id, product_list)`:包含订单ID和产品ID列表(如 `"A,B,C"`) - **产品表** `Products(product_id, price)`:包含产品ID和价格 需要将订单中的产品列表拆分成单个产品,并与产品表关联获取价格。 #### 2. **完整代码示例(Flink SQL)** ```sql -- 注册表函数:拆分字符串为多行 CREATE TEMPORARY FUNCTION SplitProducts AS 'com.example.udf.SplitFunction'; -- 创建订单表 CREATE TABLE Orders ( order_id STRING, product_list STRING ) WITH (...); -- 指定数据源连接器 -- 创建产品表 CREATE TABLE Products ( product_id STRING, price DECIMAL(10, 2) ) WITH (...); -- 指定数据源连接器 -- 使用 CROSS JOIN LATERAL 连接 SELECT o.order_id, p.product_id, pr.price FROM Orders AS o CROSS JOIN LATERAL TABLE(SplitProducts(o.product_list)) AS p(product_id) -- 拆分产品列表 JOIN Products AS pr ON p.product_id = pr.product_id; -- 关联产品价格 ``` #### 3. **表函数实现(Java)** ```java import org.apache.flink.table.functions.TableFunction; import org.apache.flink.types.Row; public class SplitFunction extends TableFunction<Row> { public void eval(String productList) { if (productList != null) { for (String product : productList.split(",")) { collect(Row.of(product.trim())); // 输出拆分后的每个产品 } } } } ``` #### 4. **关键点说明** 1. **表函数作用**: - `SplitProducts(o.product_list)` 将 `"A,B,C"` 拆分为三行:`[A]`、`[B]`、`[C]` - 通过 `CROSS JOIN LATERAL` 将每个订单与拆分结果连接 2. **连接逻辑**: ```plaintext 原始订单: [order_1, "A,B"] ↓ LATERAL 展开 → [order_1, A] → [order_1, B] ↓ 关联产品表 → [order_1, A, 10.0] (假设A的价格是10) → [order_1, B, 15.0] ``` 3. **与普通 JOIN 的区别**: - `CROSS JOIN LATERAL` 依赖左侧表的每一行生成结果 -函数可为单行输入生成多行输出(类似 `flatMap`) - 普通 JOIN 无法实现行级扩展功能 #### 5. **流处理注意事项** - 需启用状态保留:`table.exec.state.ttl=3600000`(防止状态无限增长) - 水位线传播:确保输入表的水位线正确传播到结果表 - 处理乱序事件:建议在源表定义 `WATERMARK` #### 典型应用场景 1. 解析嵌套 JSON/数组数据 2. 机器学习特征展开(如将向量拆分为多列) 3. 日志字段拆分(如将 CSV 日志拆分为结构化字段) [^1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值