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分析源码的一些宏标记设定.在已经进行了类型系统整体的设计之后,本文将开始讨论接下来的步骤.暂时不讨 ...
随机推荐
- npm i 与 npm install 的区别
我们在平时运用的时候一般用 npm i 来代替 npm install(为npm i 的简写) 但是在实际应用中两者是有些不同的(查阅总结): 1.使用npm i 安装的模块和依赖,使用npm uni ...
- 2022-12-05:部门工资前三高的所有员工。编写一个SQL查询找出每个部门中收入前三高的员工 。 +------------+----------+--------+ | Department |
2022-12-05:部门工资前三高的所有员工.编写一个SQL查询找出每个部门中收入前三高的员工 . ±-----------±---------±-------+ | Department | Em ...
- 2020-08-11:一颗现代处理器,每秒大概可以执行多少条简单的MOV指令,有哪些主要的影响因素?
福哥答案2020-08-11: [知乎答案](https://www.zhihu.com/question/413389230)MOV 指令将源操作数复制到目的操作数,是最基本的指令.首先就和CPU主 ...
- 2021-10-07:将有序数组转换为二叉搜索树。给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。高度平衡 二叉树是一棵满足「每个节点的左右两个子树
2021-10-07:将有序数组转换为二叉搜索树.给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树.高度平衡 二叉树是一棵满足「每个节点的左右两个子树 ...
- 文心一言 VS 讯飞星火 VS chatgpt (26)-- 算法导论5.1 1题
一.证明:假设在过程 HIRE-ASSISTANT 的第 4 行中,我们总能决定哪一个应聘者最佳.则意味着我们知道应聘者排名的全部次序. 文心一言: 证明: 假设在过程 HIRE-ASSISTANT ...
- Java的标识符、关键字、 常量、变量、数据类型、 数据类型转换、字符ASCII码表(基础语法的学习)
一.Java的关键字和标识符 关键字 1.定义: 在Java程序中,已经定义好的被预先使用的一些特殊的单词称为关键字,一共有50个关键字(48+2个保留字),关键字都是小写的英文单词 2.关键字的分类 ...
- ENVI5.6 安装教程,新手入门(超详细)附安装包和常见问题
ENVI是一个完整的遥感图像处理平台,广泛应用于科研.环境保护.气象.农业.林业.地球科学.遥感工程.水利.海洋等领域.目前ENVI已成为遥感影像处理的必备软件,包含辐射定标.大气校正.镶嵌裁剪.分类 ...
- Framework 中使用 Toolkit.Mvvm 的生成器功能
.NET Standard是.NET APIs的正式规范,可在多个.NET实现中使用..NET Standard的动机是为了在.NET生态系统中建立更大的统一性..NET 5及更高版本采用了不同的方法 ...
- 使用containerd从0搭建k8s(kubernetes)集群
准备环境 准备两台服务器节点,如果需要安装虚拟机,可以参考<wmware和centos安装过程> 机器名 IP 角色 CPU 内存 centos01 192.168.109.130 mas ...
- 使用 Sa-Token 实现 [记住我] 模式登录、七天免登录
一.需求分析 如图所示,一般网站的登录界面都会有一个 [记住我] 按钮,当你勾选它登录后,即使你关闭浏览器再次打开网站,也依然会处于登录状态,无须重复验证密码: 本文将详细介绍在 Sa-Token中, ...