FlinkSQL类型系统
类型有什么作用, 类型可以提供编译期检查, 避免到运行期才报错.
类型
首先Flink中自己定义了一套类型, 有LogicalType
和DataType
两个表示
LogicalType
LogicalType
表示的逻辑类型, 并不涉及类型的物理表示, 会包含nullable属性.
还有两个衍生的概念 LogicalTypeFamily
和LogicalTypeRoot
LogicalTypeRoot
表示的LogicalType的元类型, 例如TIMSTAMP
类型可以包含多种精度, 但是多种精度的TIMESTAMP(3)``TIMSTAMP(6)
都属于 TIMESTAMP_WITHOUT_TIME_ZONE
root类型. 一个LogicalType只属于一个LogicalTypeRoot
LogicalTypeFamily
表示的是LogicalTypeRoot
的归类, 一个LogicalTypeRoot
可以同时属于多个LogicalTypeFamily
例如 这就表示 DECIMAL
属于 PREDEFINED, NUMERIC, EXACT_NUMERIC的分类.
DECIMAL(
LogicalTypeFamily.PREDEFINED,
LogicalTypeFamily.NUMERIC,
LogicalTypeFamily.EXACT_NUMERIC),
DataType
DataType就在LogicalType之上添加了类型的物理表示, 可以看到他的成员变量就是由 logicalType + conversionClass组成, conversionClass表示Flink该怎么去读写这个类型.
他主要用在和外部系统交互的时候, 比如我定义一个TIMSTAMP
类型, 我可以用java.sql.Timestamp
表示, 也可以用 java.time.LocalDateTime
来表示, 这时候就可以通过显示的指定 conversionClass来实现, 如果不指定, 就使用LogicalType中的默认的conversionClass.
protected final LogicalType logicalType;
protected final Class<?> conversionClass;
例如在表示Row类型时, Flink内部都是表示为RowData, 并且内部的String都是表示为StringData
, 那么通过LogicalTypeUtils#toInternalConversionClass
就可以将相应的conversionClass转化成内部的表示. 并 DataType#bridgeTo
用来修改此DataType的物理类型表示.
当然并不是随便bridge一个类型就能生效, 他会通过LogicalType supportsInputConversion
和 supportsOutputConversion
来校验你设置的conversionClass在不在这个范围之内
比如 VarcharType
所支持的输入输出类型是
String.class.getName(), byte[].class.getName(), StringData.class.getName()
主要需要校验的点就是Source/LookupJoin/Sink/UDF 这些和外部用户定义类型需要交互的地方.
在 DynamicTableSource.Context中的 createDataStructureConverter
就是将外部系统中的数据转化成内部的数据, 而这里首先就需要表示这些外部数据类型的conversionClass是满足supportsInputConversion
的 然后通过DataStructureConverters.getConverter(producedDataType)
创建外部Object到内部类型的映射, 这样外部Java类型系统就和SQL中的RowData关联上了.
举个例子
putConverter(
LogicalTypeRoot.VARCHAR, String.class, constructor(StringStringConverter::new));
putConverter(
LogicalTypeRoot.VARCHAR, byte[].class, constructor(StringByteArrayConverter::new));
putConverter(LogicalTypeRoot.VARCHAR, StringData.class, identity());
这里对于VARCHAR
类型, 如果你在插件端中的实际Java object类型是String.class
那么就通过StringStringConverter
转化成内部的StringData
类型. 如果是byte[].class
那通过StringByteArrayConverter
来转化, 输出侧也是一样.
对于UDF呢也是如此, 不过是通过Codegen的方式来生成converter, 将用户UDF中的外部类型, 转化成内部类型. 如果用户类型是非标准SQL的类型, 也支持指定成RAW来表示成 RawValueData
RowData
RowData是Flink内部的数据类型, 可以看到上面, toInternal转化的时候就是要将外部的ROW 转成RowData, 他和SQL类型的映射关系, 这可以理解成数据类型的内部物理表示层. 内部SQL算子, Codegen代码都是面向RowData编程, 而RowData 也有多种实现有基于Object的GenericRowData, 有基于二进制表示的BinaryRowData, 也有基于列式视图的 ColumnarRowData
* +--------------------------------+-----------------------------------------+
* | SQL Data Types | Internal Data Structures |
* +--------------------------------+-----------------------------------------+
* | BOOLEAN | boolean |
* +--------------------------------+-----------------------------------------+
* | CHAR / VARCHAR / STRING | {@link StringData} |
* +--------------------------------+-----------------------------------------+
* | BINARY / VARBINARY / BYTES | byte[] |
* +--------------------------------+-----------------------------------------+
* | DECIMAL | {@link DecimalData} |
* +--------------------------------+-----------------------------------------+
* | TINYINT | byte |
* +--------------------------------+-----------------------------------------+
* | SMALLINT | short |
* +--------------------------------+-----------------------------------------+
* | INT | int |
* +--------------------------------+-----------------------------------------+
* | BIGINT | long |
* +--------------------------------+-----------------------------------------+
* | FLOAT | float |
* +--------------------------------+-----------------------------------------+
* | DOUBLE | double |
* +--------------------------------+-----------------------------------------+
* | DATE | int (number of days since epoch) |
* +--------------------------------+-----------------------------------------+
* | TIME | int (number of milliseconds of the day) |
* +--------------------------------+-----------------------------------------+
* | TIMESTAMP | {@link TimestampData} |
* +--------------------------------+-----------------------------------------+
* | TIMESTAMP WITH LOCAL TIME ZONE | {@link TimestampData} |
* +--------------------------------+-----------------------------------------+
* | INTERVAL YEAR TO MONTH | int (number of months) |
* +--------------------------------+-----------------------------------------+
* | INTERVAL DAY TO MONTH | long (number of milliseconds) |
* +--------------------------------+-----------------------------------------+
* | ROW / structured types | {@link RowData} |
* +--------------------------------+-----------------------------------------+
* | ARRAY | {@link ArrayData} |
* +--------------------------------+-----------------------------------------+
* | MAP / MULTISET | {@link MapData} |
* +--------------------------------+-----------------------------------------+
* | RAW | {@link RawValueData} |
* +--------------------------------+-----------------------------------------+
TypeInformation
Flink还有一套比较早的类型系统 基于TypeInfomation, 用于兼容较早的Source/Sink 以及UDF的类型接口, 通过LegacyTypeInfoDataTypeConverter
将TypeInfomation转化成新的DataType来使用.
RelDataType
以上这三种类型都是Flink中的类型体系, 最终Flink还是通过Calcite来进行sql validate, 而Calcite中的类型是RelDataType, 所以需要有这样的一个映射关系. 通过FlinkTypeFactory#createFieldTypeFromLogicalType
可以将LogicalType转化成RelDataType
Validate
首先在validate阶段, calcite系统就会去推断各个字段, 函数入参, 返回值的类型, 来进行语法的校验.
表的schema类型
flink中的catalog和Calcite的CalciteSchema通过CatalogManagerCalciteSchema
来打通, 将CatalogManager
封装到SimpleCalciteSchema
中, 所以最后的表的查找都会通过 FlinkCalciteCatalogReader#getTable
路由到
CatalogManagerCalciteSchema -> CatalogCalciteSchema -> DatabaseCalciteSchema 最终是从Catalog中查找到相应的表(CatalogSchemaTable) 这个是Flink Catalog table 和Calcite Table的中间桥接.
而此时就需要将Flink Catalog table 的表类型 (DataType) 转化成 Calcite中的类型RelDataType. 在CatalogSchemaTable#getRowType
中就会完成这一转化, 最终源表产出的是一个ROW struct type.
final List<String> fieldNames = schema.getColumnNames();
final List<LogicalType> fieldTypes =
schema.getColumnDataTypes().stream()
.map(DataType::getLogicalType)
.map(PlannerTypeUtils::removeLegacyTypes)
.collect(Collectors.toList());
return flinkTypeFactory.buildRelNodeRowType(fieldNames, fieldTypes);
函数定义
基于Calcite SqlFunction的定义
首先Flink从Calcite中集成了很多函数定义例如在 FlinkSqlOperatorTable
中
public static final SqlOperator AND = SqlStdOperatorTable.AND;
public static final SqlOperator AS = SqlStdOperatorTable.AS;
SqlOperandTypeInference推断入参的类型 他的入参是 SqlCallBinding, RelDataType returnType, RelDataType[] operandTypes (这个入参都是unknown, 需要实现者去填充). InferTypes内置入参类型推断
SqlReturnTypeInference推断函数的返回类型. 入参是SqlOperatorBindingbinding实际上就是把校验时的上下文, 如入参类型, 入参个数, 入参的常量提供给你, 方便你去做类型推断.Calcite ReturnTypes和 FlinkReturnTypes提供了很多预置的返回类型推断的实现
SqlOperandTypeChecker 检查入参类型 个数等等是否合法
例如我写一个 SELECT A and B from myTable 测试a和b不是boolean类型. 首先在validate的过程中需要推断SELECT列表的类型, A and B 是一个 SqlOperator 首先会调用上面的SqlOperandTypeInference推导参数类型. 对于AND操作, 推导入参都是boolean
然后会根据每一个SqlNode#deriveType 推导其返回类型. 而递归查找的过程中最后会查找到source table schema 中找到相应的字段推断出A和B分别是什么类型. 所以感觉第一步的SqlOperandTypeInference并没有什么用 ?
最后推导出入参类型之后会通过SqlOperandTypeChecker校验入参类型是否符合预期.
因此当A和B类型不是Boolean时, 通过Checker就会校验报错
基于BuiltInSqlFunction接口
里面所使用的接口其实还是前面 Calcite里面定义的几种推导和校验的接口. BuiltInSqlFunction的接口的主要作用是承接一些 BuiltInFunctionDefinition 不支持的定义方式. 作者希望的主要内置函数的入口还是第三种
基于BuiltInFunctionDefinition
这个就是Flink自己完全新定义了一套类型的dsl. 如 InputTypeStrategy, OutputTypeStrategy. 这个构建出来的Function并不是直接的Calcite的SqlFunction. 最终这些函数都会注册到FunctionCatalogOperatorTable
中. 而默认的Calcite初始化的OperatorTable就是 FunctionCatalogOperatorTable
和 FlinkSqlOperatorTable
return SqlOperatorTables.chain(
new FunctionCatalogOperatorTable(
context.getFunctionCatalog(),
context.getCatalogManager().getDataTypeFactory(),
typeFactory),
FlinkSqlOperatorTable.instance());
所以查找函数的时候会首先去FunctionCatalogOperatorTable new stack中查找. 查找到的BuiltInFunctionDefinition会通过convertToSqlFunction转化成Calcite的接口. 最终转化成为BridgingSqlFunction, BridgingSqlAggFunction
基于CallExpression
- CallExpresssion的定义包含了函数的主体
FunctionDefinition
, 参数ResolvedExpression
- 这一套接口主要可以透传到插件端, 比如Filter下推时下推的就是
ResolvedExpression
, 以及Watermark表达式 - 以及在Java scala的table api [[ImplicitExpressionOperations]]中提供dsl api
- 这一套接口主要可以透传到插件端, 比如Filter下推时下推的就是
- 最后这套CallExpression会通过一系列的转化规则
FunctionDefinitionConvertRule
DirectConvertRule
等等转化成Calcite的RexNode, 用于在Plan阶段将这些边缘节点(用户交互界面)的Expression编译成RexNode. 里面的转化逻辑实际上和上面的三种基本映射
自定义函数
- 注册用户自定义函数的入口是
TableEnvironmentImpl#createSystemFunction
最终实例化成FunctionDefinition
- 类型校验则是通过
ScalarFunction#getTypeInference
来完成的, 用户实际上也可以覆写这个方法来完成类型的校验, 他也提供了一个默认的实现. 这个默认的实现就是一套基于函数hint的类型推导.FunctionMappingExtractor
完成对自定义函数的分析, 如入参类型, 返回值, 函数名称等等. 这里面用户就可以通过DataTypeHint 来对参数和返回值进行标记. 完成提取后, 用户参数的校验逻辑就和前面的BuiltInFunctionDefinition
一致了
函数实现
- 函数实现分为两种Old stack中, 对于Calcite中定义的函数, Flink提供运行时实现基本是通过Codegen代码来生成的, 所以之前新增一个函数比较繁琐, 需要修改Codegen的逻辑, 主要涉及的地方就是 ExprCodeGenerator
- 而针对New stack的函数, 一般只需要开发者提供一个runtime class, 通过 BridgingSqlFunctionCallGen 完成函数实现的注入.
时间属性类型
- 时间属性主要是用作窗口时间的特殊标记, 标记是proctime或者eventime. 所以有一种特殊的类型叫做
TimeIndicatorRelDataType
这里主要是在源表定义的地方会对时间属性字段做特殊处理插入相应的TimeKind. - 需要注意的一点是针对时间属性字段需要做一些物化处理. 在
RelTimeIndicatorConverter
完成 例如- 聚合参数或者group维度 中的时间属性字段
- Sink节点下发前
- Calc计算节点
FlinkSQL类型系统的更多相关文章
- javascript中15种原生对象类型系统综述
前面的话 在编程语言中,能够表示并操作的值的类型称做数据类型,编程语言最基本的特性就是能够支持多种数据类型.javascript拥有强大的类型系统,主要包括原生对象.宿主对象和浏览器拓展对象,本文主要 ...
- TypeScript - 基本类型系统
对于程序来说我们需要基本的数据单元,如:numbers, strings, structures, boolean 等数据结构.在TypeScript中我们支持很多你所期望在JavaScript中所拥 ...
- 基于类型系统的面向对象编程语言Go
(整理自网络) 面向对象编程 Go语言的面向对象编程(OOP)非常简洁而优雅.说它简洁,在于它没有了OOP中很多概念,比如:继承.虚函数.构造函数和析构函数.隐藏的this指针等等.说它优雅,是它的面 ...
- 04.C#类型系统、值类型和引用类型(二章2.2-2.3)
今天要写的东西都是书中一些概念性的东西,就当抄笔记,以提问对话的方式将其写出来吧,说不定以后面试能有点谈资~~~ Q1.C#1系统类型包含哪三点特性? A1.C#1类型系统是静态的.显式的和安全的. ...
- javascript类型系统之Array
原文:javascript类型系统之Array 目录 [1]数组创建 [2]数组操作 [3]继承的方法 [4]实例方法 数组转换 数组检测 栈和队列 排序方法 操作方法 位置方法 前面的话 数组是一组 ...
- .NET Framework 中的类型系统的两个基本点
它支持继承原则. 类型可从称为基类型的其他类型派生. 派生类型继承基类型的方法.属性和其他成员(存在一些限制). 之后,基类型可从某些其他类型派生,这种情况下,派生类型继承其层次结构中这两个基类型的成 ...
- 《InsideUE4》UObject(三)类型系统设定和结构
垃圾分类,从我做起! 引言 上篇我们谈到了为何设计一个Object系统要从类型系统开始做起,并探讨了C#的实现,以及C++中各种方案的对比,最后得到的结论是UE采用UHT的方式搜集并生成反射所需代码. ...
- c#1所搭建的核心基础之类型系统的特征
类型系统的特征简介 几乎每种编程语言都有某种形式的一个类型系统.类型系统大致被分为:强/弱,安全/不安全,静态/动态,显式/隐式等类型. c#在类型系统世界中的位置 c#1的类型系统是静态的.显式的和 ...
- Haskell 笔记(三)类型系统
类型 (Type) Haskell的类型系统式静态类型系统,在编译的时候就知道数据类型,所以不同类型的值运算在编译的时候就会报错,比如用布尔值和整数运算,在C语言中这种运算就不会报错. Haskell ...
- 《InsideUE4》UObject(四)类型系统代码生成
你想要啊?想要你就说出来嘛,你不说我怎么知道你想要呢? 引言 上文讲到了UE的类型系统结构,以及UHT分析源码的一些宏标记设定.在已经进行了类型系统整体的设计之后,本文将开始讨论接下来的步骤.暂时不讨 ...
随机推荐
- Maven工程开发
Maven工程开发 编写pom.xml配置文件 <properties></properties>定义项目的基本信息 groupId 反向域名,定义项目组名 artifactI ...
- json函数
Python与JSON(load.loads.dump.dumps) 1.Python中加载JSON 使用loads(string):作用将string类型转为dict字典或dict链表 # 加载 ...
- switch条件语句2
/*switch条件语句*/ #include<stdio.h> int main() { while (1) { int num; printf("请输入你的选择编号:\n&q ...
- 计算机网络OSI七层参考模型和tcp/udp五层参考模型
计算机网络OSI七层参考模型和tcp/udp五层参考模型 目录 一.OSI七层参考模型和TCP/UDP五层参考模型 1.应用层 2.表示层 3.会话层 4.传输层 5.网络层 6.数据链路层 7.物理 ...
- IT入门深似海,入门到放弃你学废了嘛
我一直觉得IT行业 程序员行业.甚至觉得程序员人群 是一个特殊存在的群体. 入门到放弃,是真的,IT门槛高嘛. 其实吧,IT编程门槛,是有的,但是对于感兴趣的,想学习IT编程同学来说,也是一件容易事情 ...
- SLAM系统--开启摄像头连接
博客地址:https://www.cnblogs.com/zylyehuo/ 基于ORB-SLAM3库搭建SLAM系统详见之前的博客 基于ORB-SLAM3库搭建SLAM系统 - zylyehuo - ...
- Hive执行计划之一文读懂Hive执行计划
目录 概述 1.hive执行计划的查看 2.学会查看Hive执行计划的基本信息 3.执行计划步骤操作过程 4.explain extended 概述 Hive的执行计划描述了一个hiveSQL语句的具 ...
- 记一次BootCDN被黑产挂马导致站点跳转博彩网站的问题
近期发现公司某些站点出现偶尔跳转博彩网站的现象,经过排查发现该现象为供应链投毒攻击,BootCDN上的静态资源无一例外均被污染, 当外站引入BootCDN的静态资源时,如果请求携带的Referer头为 ...
- Vue_Django 登录注册+图书管理系统
Vue前端 注册页面 点击查看代码 <template> <div class="register"> <el-row :gutter="2 ...
- 【.NET深呼吸】将XAML放到WPF程序之外
上一篇水文中,老周说了一下纯代码编写 WPF 的大概过程.不过,还是不够的,本篇水文中咱们还要更进一步. XAML 文件默认是作为资源打包进程序中的,而纯代码编写又导致一些常改动的东西变成硬编码了.为 ...