将新数据类型添加到SQL查询引擎(Trino)
#sql #database #java #systems

介绍

在这篇文章中,我想带您进行一次旅程,该旅程从Github Issue #1284开始,要求对Trino的时间戳中的纳秒/微秒精度支持,并以Github PR #3783的合并结束,以增加对Trino查询引擎的参数时间戳类型的支持。

此旅程也包括许多惊喜!

例如,如Github Issue #34中的许多细节所述,在Trino上的时间戳类型的语义上的问题识别。

我们的目标是深入SQL查询引擎的一些重要维度,包括类型系统和数据编码。

,但还可以品尝到要设计一个复杂的系统所需的内容,例如分布式SQL查询引擎,该引擎每天都被数千用户使用。

这将是一个很长的帖子,所以扣紧!

问题

在像计算机这样的数字系统中使用时间时,精度是我们非常关心的事情之一。尤其是当时间对我们要执行的任务很重要时。

一些像财务这样的行业对时间更敏感
测量比其他人,但无论如何,我们都希望很好地了解与我们合作的时间数据类型的语义。

2020年,FINRA也被称为金融业监管机构,向SEC提交了一项建议,要求在贸易报告中更改与时间戳有关的规则。

该提案建议使用纳秒时间粒度开始跟踪交易。

一个支持纳秒时间戳精度的系统,
可以在不丢失任何信息的情况下使用毫秒精度的时间戳。相反的不是真的。

Trino可以支持picosecond timestamp precision

Trino是金融部门大量使用的技术。
这是Trino社区的reasons开始着眼于Trino中的时间戳类型。

SQL数据类型

数据类型是一组代表值,任何值的物理表示都取决于实现。

时间戳的值可能是1663802507,但是如何实际表示的时间戳是数据库工程师来决定。

通常,DateTime类型(包括时间戳)使用32/64位整数进行物理实现。

SQL规范允许DateTime类型的任意精度,并通过说明以下内容:

...第二,
可以定义为指示维持十进制数字的数量
遵循秒值中的小数点,一个非阴性精确的数字值。

参数化数据类型

如果您熟悉SQL,您将很容易识别以下语句:

CREATE TABLE orders (
orderkey bigint
)

上面将创建一个名为orderkeke的列的表,其类型将为bigint。我们无法参数化bigint。

对于大多数类型来说都是如此,但是有几个例外,时间戳是其中之一。

由于我们以后会进行调查的原因,当您处理毫秒与picseconds的时间戳时,它确实有所作为。

我们想允许用户定义粒度,并利用它在优化数据的操纵方式中。

在Trino中,时间戳类型看起来像该时间戳(P),
其中p是秒数的精度数字数。

以下SQL语句:

CREATE TABLE orders (
orderkey bigint,
creationdate timestamp(6)
)

将创建一个名为Type Timestamp类型的creationDate的列订单,秒数为6位精度。

添加新数据类型

添加新类型,或者在这种情况下,用新语义更新现有的类型是一项风险的任务。

我们正在处理一个每天都在使用数千个用户使用的系统,而对某种类型的任何更改可能最终都会出现数千个SQL查询。

让我们看看Trino团队在设计阶段的一些考虑因素,然后我们将在
上详细介绍 他们每个人。

首先我们有性能考虑。
我们如何确保在处理时间戳时提供最好的表现?
这里的诀窍是根据用户定义的精度考虑不同的表示和函数。

是向后兼容的主要问题。

我们如何确保当前实现的时间戳语义不会随着参数化的引入而破裂?

此兼容性不仅适用于类型本身,还适用于与此类型相关的所有功能。

然后,我们使Trino的附加复杂性是联合查询引擎。

我们需要确保可以正确地映射新的时间戳类型,并以最大的方式进行每个连接器的等效类型。

最后,我们需要确保我们正确处理不同类型和精确度之间的数据类型转换。

上述考虑因素中的每一个都是一个巨大的引人入胜的话题,所以让我们潜水!

从逻辑到物理表示

我们使用整数实际实现时间戳。
显然,一个32位整数可以代表比8bit的时间戳更多的时间戳。

那么我们需要多少位才能获得Picsecond Precision?

The answer can be found in the design document for the variable precision datetime types.

对于p=12,我们需要79位,这是64位以上,而Long Java type supports是什么。要高于p=7分辨率时间戳,我们将需要提出自己的Java表示。

Trino做什么是以下内容:

我们将对任何可以用Java长类型表示的时间戳精度进行一个简短编码。这是针对p<=6

我们还将针对任何时间戳Precision p > 6及我们要支持的最大值引入长期编码,在这种情况下为12。

分数部分将为16或32,具体取决于我们要在末尾支持的精度。

实现时看起来如何?

Let's see the current Trino implementation

Longtimestamp类实现了我们在上图中描述的类型,如您所见,有一种长Java类型用于保存秒,然后是INT Java类型(这是32位类型),用于表示秒的分数部分,精度高达32位。

也有一个简短时间戳的实现。

let's see how this looks like in Trino

我们希望简短的时间戳由64位整数代表,并这样做,我们将此类明确将其与Java Long.Class相关联。

大部分实现已被省略,以使很容易理解时间戳如何表示,但是Writelong方法是看如何假定类型的好地方。

您可能想知道为什么我们如此努力地使我们的生命如此努力,而我们不仅仅拥有一个时间戳物理表示来简化事情。

答案是性能。 Trino是行业中表现最佳的SQL查询引擎之一,如果我们不挤压所有的表现,这不会发生。

当我们谈论性能时,我们有两种类型。一个是存储性能,例如我们如何减少存储信息所需的存储空间。

另一个是处理时间性能,或者我们如何使事情运行速度更快。

节省空间和时间

If we take a look at the LongTimestampType implementation

trino正在使用一个blockbuilder编写和读取特定类型的数据。
以上代码段中有趣的是:

  • 该类型的大小用作传递给Blockuilder的信息
  • 有一个特殊实施的INT96的BlockBuilder,如果您记得的话,我们较早地得出结论,我们将需要64 + 32 = 96位来表示我们的长度戳记。

如果我们看一下ShortTimestampType implementation,我们会注意到所使用的BlockBuilder不同。

您会注意到所使用的API是相同的,但是使用的BlockBuilder不同,现在我们使用longarrayblockbuilder而不是INT96ArrayBlockBuilder。

Trino SPI的块部分是查询引擎的非常重要的部分,因为它负责有效编码已处理的数据

块API中的任何决定都会极大地影响引擎的性能。

现在,您可能想知道为什么我要经历所有这些示例,以及为什么Trino工程师经历了不同的块作家的实现。

原因很简单,它与数据类型的空间效率有关。

请记住,ShorttimestAmpType由64个长Java类型表示,而Longtimestamptype则由64个长Java类型加上32 INT Java类型表示。基于此,存储每种类型的TIMESTAM所需的内存是:

  • shorttimestpype:8字节
  • longtimestamptype:16字节

我们需要两倍的时间戳,其精度比微秒多。
请记住,如果我们使用longtimestampype,即使我们没有picsecond Precision,我们也会使用16个字节。

这是记忆中需求的2倍,当我们使用数据b的时,这是很多!

让我们现在看看时间复杂性发生了什么,以及是否有任何典型类型操作的性能差异,例如两种时间戳类型之间的比较。

为此,我们需要实施ShortTimestampTypeLongTimestampType,让我们检查比较器的差异。

Shorttimestampype比较器涉及一种使用long.compare方法的比较。

另一方面,Longtimestamptype可能有可能进行第二个比较,因此在最坏的情况下,我们必须比较一种长和一种int类型。

通过采用这种更复杂,双重类型的时间戳实现,时间和存储都有大量的性能。

在引擎的每个部分都这样做的决定,就是使Trino这样的表演者查询引擎的原因。

快速移动,但不要破坏东西

Trino每天都有数千名用户使用,因此每当将新事物引入E
时,这一点都非常重要。 没有什么都不会破裂的。

向后兼容性很重要,它是任何新功能设计的一部分。

为了确保向后兼容性,决定首先通过在添加参数化的同时维护当前语义来解决语言和数据类型。

通过当前语义,我们的意思是当时Trino支持的精度为p = 3。

单独更新类型是不够的,有许多特殊功能必须被参数化。

这些功能是Current_time,locatime,current_timestamp和localtimestamp,在其中用户可以在支持当前语义作为默认语义的同时提供精度参数。

与上述功能一起,所有接受DDL中时间戳类型的连接器都必须与所有在这些类型上运行的功能和操作员一起更新。

这只是构建对参数化时间戳类型的支持的第一步,此步骤的目的是引入适当的更改,而不必通过执行正确的默认值向现有用户打破任何内容。

我们还需要确保有一种方法可以安全地施放具有P1精度的时间戳,以使用P2精度的时间戳。

为此,我们需要实现截断和舍入的逻辑。

这些操作的逻辑可以在Timestamps类的实现中找到。

上面的两种方法正在实现将micros截断为millis(第一个)的逻辑,并将micros舍入milos(第二种)。

时间是相对的

Trino是一种联合查询引擎,这意味着它允许某人执行将其推向不同系统的查询。

这些系统通常不共享相同的语义。日期/时间类型尤其如此。

对于Trino考虑连接器的新功能,首先必须为Hive实施并发布。

hive很重要,因为它与Trino一起被大量使用,但是它也是任何其他连接器的参考实现。

第一步是添加对Hive Connector的variable-precision timestamp类型的支持。您可能会在此问题上注意到,Hive Metastore使用可选的nanosecond precision支持时间戳。

这意味着,尽管Trino可以处理Picsecond Time分辨率,但是在与Hive一起工作时,我们只能使用纳秒。

这是一个常见的模式,不同的数据存储将具有不同的限制,例如,PoStrgres连接器可以处理至微秒分辨率。

这意味着对Postgres连接器的新时间戳参数的支持的人必须考虑到这一点,并确保发生正确的类型铸件。

在Hive得到支持后,遵循了许多其他连接器和客户端,例如更新CLI,JDBC和Python客户。这些是一些最常用的连接器和客户端。

结论

当您使用时间数据时,总会有折衷,在大多数情况下,您必须在建模和分析数据时多次更改时间的方式。

查询引擎的责任是为您提供所需的所有工具,同时确保数据处理的健全性和正确性。

既然您对Trino类型的工作方式有了基本的了解,我鼓励您更深入地了解代码库,并更深入地研究这些类型如何序列化然后处理。